diff --git a/extensions/kallichore-adapter/package.json b/extensions/kallichore-adapter/package.json index cc3064401be..eb38dc5d3d1 100644 --- a/extensions/kallichore-adapter/package.json +++ b/extensions/kallichore-adapter/package.json @@ -34,6 +34,11 @@ "default": false, "description": "%configuration.enable.description%" }, + "kallichoreSupervisor.showTerminal": { + "type": "boolean", + "default": false, + "description": "%configuration.showTerminal.description%" + }, "kallichoreSupervisor.logLevel": { "scope": "window", "type": "string", @@ -84,7 +89,7 @@ }, "positron": { "binaryDependencies": { - "kallichore": "0.1.13" + "kallichore": "0.1.15" } }, "dependencies": { diff --git a/extensions/kallichore-adapter/package.nls.json b/extensions/kallichore-adapter/package.nls.json index 197672e8318..0a5784e3ef5 100644 --- a/extensions/kallichore-adapter/package.nls.json +++ b/extensions/kallichore-adapter/package.nls.json @@ -7,7 +7,8 @@ "configuration.logLevel.debug.description": "Debug messages", "configuration.logLevel.trace.description": "Verbose tracing messages", "configuration.logLevel.description": "Log level for the kernel supervisor (restart Positron to apply)", - "configuration.enable.description": "Run Jupyter kernels under the Kallichore supervisor", + "configuration.enable.description": "Run Jupyter kernels under the Kallichore supervisor in Positron Desktop. (The supervisor is always enabled in server configurations.)", + "configuration.showTerminal.description": "Show the host terminal for the Kallichore supervisor", "configuration.attachOnStartup.description": "Run before starting up Jupyter kernel (when supported)", "configuration.sleepOnStartup.description": "Sleep for n seconds before starting up Jupyter kernel (when supported)" } diff --git a/extensions/kallichore-adapter/src/KallichoreAdapterApi.ts b/extensions/kallichore-adapter/src/KallichoreAdapterApi.ts index eee803eeb14..e13eae14e5c 100644 --- a/extensions/kallichore-adapter/src/KallichoreAdapterApi.ts +++ b/extensions/kallichore-adapter/src/KallichoreAdapterApi.ts @@ -183,6 +183,9 @@ export class KCApi implements KallichoreAdapterApi { // Start a timer so we can track server startup time const startTime = Date.now(); + // Consult configuration to see if we should show this terminal + const showTerminal = config.get('showTerminal', false); + // Start the server in a new terminal this._log.info(`Starting Kallichore server ${shellPath} on port ${port}`); const terminal = vscode.window.createTerminal({ @@ -191,7 +194,7 @@ export class KCApi implements KallichoreAdapterApi { shellArgs: ['--port', port.toString(), '--token', tokenPath], env, message: `*** Kallichore Server (${shellPath}) ***`, - hideFromUser: false, + hideFromUser: !showTerminal, isTransient: false }); @@ -211,12 +214,20 @@ export class KCApi implements KallichoreAdapterApi { this._log.info(`Kallichore ${status.body.version} server online with ${status.body.sessions} sessions`); break; } catch (err) { + const elapsed = Date.now() - startTime; + // ECONNREFUSED is a normal condition during startup; the server // isn't ready yet. Keep trying until we hit the retry limit, // about 2 seconds from the time we got a process ID // established. if (err.code === 'ECONNREFUSED') { if (retry < 19) { + // Log every few attempts. We don't want to overwhelm + // the logs, and it's normal for us to encounter a few + // connection refusals before the server is ready. + if (retry % 5 === 0) { + this._log.debug(`Waiting for Kallichore server to start (attempt ${retry + 1}, ${elapsed}ms)`); + } // Wait a bit and try again await new Promise((resolve) => setTimeout(resolve, 50)); continue; @@ -226,7 +237,16 @@ export class KCApi implements KallichoreAdapterApi { throw new Error(`Kallichore server did not start after ${Date.now() - startTime}ms`); } } - this._log.error(`Failed to get session list from Kallichore; ` + + + // If the request times out, go ahead and try again as long as + // it hasn't been more than 10 seconds since we started. This + // can happen if the server is slow to start. + if (err.code === 'ETIMEDOUT' && elapsed < 10000) { + this._log.info(`Request for server status timed out; retrying (attempt ${retry + 1}, ${elapsed}ms)`); + continue; + } + + this._log.error(`Failed to get initial server status from Kallichore; ` + `server may not be running or may not be ready. Check the terminal for errors. ` + `Error: ${JSON.stringify(err)}`); throw err; diff --git a/extensions/kallichore-adapter/src/KallichoreSession.ts b/extensions/kallichore-adapter/src/KallichoreSession.ts index 41cb31bd2d7..9c313b88725 100644 --- a/extensions/kallichore-adapter/src/KallichoreSession.ts +++ b/extensions/kallichore-adapter/src/KallichoreSession.ts @@ -12,7 +12,7 @@ import { JupyterKernelExtra, JupyterKernelSpec, JupyterLanguageRuntimeSession } import { ActiveSession, DefaultApi, HttpError, InterruptMode, NewSession, StartupError, Status } from './kcclient/api'; import { JupyterMessage } from './jupyter/JupyterMessage'; import { JupyterRequest } from './jupyter/JupyterRequest'; -import { KernelInfoRequest } from './jupyter/KernelInfoRequest'; +import { KernelInfoReply, KernelInfoRequest } from './jupyter/KernelInfoRequest'; import { Barrier, PromiseHandles, withTimeout } from './async'; import { ExecuteRequest, JupyterExecuteRequest } from './jupyter/ExecuteRequest'; import { IsCompleteRequest, JupyterIsCompleteRequest } from './jupyter/IsCompleteRequest'; @@ -36,6 +36,7 @@ import { CommMsgRequest } from './jupyter/CommMsgRequest'; import { DapClient } from './DapClient'; import { SocketSession } from './ws/SocketSession'; import { KernelOutputMessage } from './ws/KernelMessage'; +import { UICommRequest } from './UICommRequest'; export class KallichoreSession implements JupyterLanguageRuntimeSession { /** @@ -77,6 +78,9 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { /** A map of pending RPCs, used to pair up requests and replies */ private _pendingRequests: Map> = new Map(); + /** An array of pending UI comm requests */ + private _pendingUiCommRequests: UICommRequest[] = []; + /** Objects that should be disposed when the session is disposed */ private _disposables: vscode.Disposable[] = []; @@ -123,11 +127,22 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { */ private _activeBackendRequestHeader: JupyterMessageHeader | null = null; + /** + * Constructor for the Kallichore session wrapper. + * + * @param metadata The session metadata + * @param runtimeMetadata The runtime metadata + * @param dynState The initial dynamic state of the runtime + * @param _api The API instance to use for communication + * @param _new Set to `true` when the session is created for the first time, + * and `false` when it is restored (reconnected). + * @param _extra Extra functionality to enable for this session + */ constructor(readonly metadata: positron.RuntimeSessionMetadata, readonly runtimeMetadata: positron.LanguageRuntimeMetadata, readonly dynState: positron.LanguageRuntimeDynState, private readonly _api: DefaultApi, - private _new: boolean, + private readonly _new: boolean, private readonly _extra?: JupyterKernelExtra | undefined) { // Create event emitters @@ -155,6 +170,7 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { this._kernelChannel = positron.window.createRawLogOutputChannel( `${runtimeMetadata.runtimeName}: Kernel`); + this._kernelChannel.appendLine(`** Begin kernel log for session ${metadata.sessionName} (${metadata.sessionId}) at ${new Date().toLocaleString()} **`); } /** @@ -199,16 +215,16 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { // Replace {log_file} with the log file path. Not all kernels // have this argument. if (arg === '{log_file}') { - fs.writeFile(logFile, '', () => { - this.streamLogFile(logFile); + fs.writeFile(logFile, '', async () => { + await this.streamLogFile(logFile); }); return logFile; } // Same as `log_file` but for profiling logs if (profileFile && arg === '{profile_file}') { - fs.writeFile(profileFile, '', () => { - this.streamProfileFile(profileFile); + fs.writeFile(profileFile, '', async () => { + await this.streamProfileFile(profileFile); }); return profileFile; } @@ -362,13 +378,32 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { callMethod(method: string, ...args: Array): Promise { const promise = new PromiseHandles; + // Create the request + const request = new UICommRequest(method, args, promise); + // Find the UI comm const uiComm = Array.from(this._comms.values()) .find(c => c.target === positron.RuntimeClientType.Ui); + if (!uiComm) { - throw new Error(`Cannot invoke '${method}'; no UI comm is open.`); + // No comm open yet? No problem, we'll call the method when the + // comm is opened. + this._pendingUiCommRequests.push(request); + this.log(`No UI comm open yet; queueing request '${method}'`, vscode.LogLevel.Debug); + return promise.promise; } + return this.performUiCommRequest(request, uiComm.id); + } + + /** + * Performs a UI comm request. + * + * @param req The request to perform + * @param uiCommId The ID of the UI comm + * @returns The result of the request + */ + performUiCommRequest(req: UICommRequest, uiCommId: string): Promise { // Create the request. This uses a JSON-RPC 2.0 format, with an // additional `msg_type` field to indicate that this is a request type // for the UI comm. @@ -379,13 +414,13 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { jsonrpc: '2.0', method: 'call_method', params: { - method, - params: args + method: req.method, + params: req.args }, }; const commMsg: JupyterCommMsg = { - comm_id: uiComm.id, + comm_id: uiCommId, data: request }; @@ -401,7 +436,7 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { // for conformity with code that expects an Error object. error.name = `RPC Error ${response.error.code}`; - promise.reject(error); + req.promise.reject(error); } // JSON-RPC specifies that the return value must have either a 'result' @@ -415,18 +450,18 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { data: {}, }; - promise.reject(error); + req.promise.reject(error); } // Otherwise, return the result - promise.resolve(response.result); + req.promise.resolve(response.result); }) .catch((err) => { this.log(`Failed to send UI comm request: ${JSON.stringify(err)}`, vscode.LogLevel.Error); - promise.reject(err); + req.promise.reject(err); }); - return promise.promise; + return req.promise.promise; } /** @@ -539,6 +574,14 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { const commOpen = new CommOpenCommand(msg, metadata); await this.sendCommand(commOpen); this._comms.set(id, new Comm(id, type)); + + // If we have any pending UI comm requests and we just created the + // UI comm, send them now + if (type === positron.RuntimeClientType.Ui) { + this.sendPendingUiCommRequests(id).then(() => { + this.log(`Sent pending UI comm requests to ${id}`, vscode.LogLevel.Trace); + }); + } } else { this.log(`Can't create ${type} client for ${this.runtimeMetadata.languageName} (not supported)`, vscode.LogLevel.Error); } @@ -564,6 +607,14 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { if (!this._comms.has(key)) { this._comms.set(key, new Comm(key, target)); } + + // If we just discovered a UI comm, send any pending UI comm + // requests to it. + if (target === positron.RuntimeClientType.Ui) { + this.sendPendingUiCommRequests(key).then(() => { + this.log(`Sent pending UI comm requests to ${key}`, vscode.LogLevel.Trace); + }); + } } } return result; @@ -628,7 +679,7 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { if (logFileIndex > 0 && logFileIndex < session.argv.length - 1) { const logFile = session.argv[logFileIndex + 1]; if (fs.existsSync(logFile)) { - this.streamLogFile(logFile); + await this.streamLogFile(logFile); break; } } @@ -659,7 +710,7 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { async start(): Promise { try { // Attempt to start the session - await this.tryStart(); + return this.tryStart(); } catch (err) { if (err instanceof HttpError && err.statusCode === 500) { // When the server returns a 500 error, it means the startup @@ -698,23 +749,33 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { this.onStateChange(positron.RuntimeState.Exited); throw err; } - - return this.getKernelInfo(); } /** * Attempts to start the session; returns a promise that resolves when the * session is ready to use. */ - private async tryStart(): Promise { + private async tryStart(): Promise { // Wait for the session to be established before connecting. This // ensures either that we've created the session (if it's new) or that // we've restored it (if it's not new). await withTimeout(this._established.wait(), 2000, `Start failed: timed out waiting for session ${this.metadata.sessionId} to be established`); + let runtimeInfo: positron.LanguageRuntimeInfo | undefined; + + // Mark the session as starting + this.onStateChange(positron.RuntimeState.Starting); + // If it's a new session, wait for it to be created before connecting if (this._new) { - await this._api.startSession(this.metadata.sessionId); + const result = await this._api.startSession(this.metadata.sessionId); + // Typically, the API returns the kernel info as the result of + // starting a new session, but the server doesn't validate the + // result returned by the kernel, so check for a `status` field + // before assuming it's a Jupyter message. + if (result.body.status === 'ok') { + runtimeInfo = this.runtimeInfoFromKernelInfo(result.body); + } } // Before connecting, check if we should attach to the session on @@ -733,10 +794,16 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { await withTimeout(this.connect(), 2000, `Start failed: timed out connecting to session ${this.metadata.sessionId}`); if (this._new) { - // If this is a new session, wait for it to be ready before - // returning. This can take some time as it needs to wait for the - // kernel to start up. - await withTimeout(this._ready.wait(), 10000, `Start failed: timed out waiting for session ${this.metadata.sessionId} to be ready`); + if (runtimeInfo) { + // If we got runtime info at startup, open the ready + // barrier immediately + this.markReady(); + } else { + // If this is a new session without runtime information, wait + // for it to be ready instead. This can take some time as it + // needs to wait for the kernel to start up. + await withTimeout(this._ready.wait(), 10000, `Start failed: timed out waiting for session ${this.metadata.sessionId} to be ready`); + } } else { if (this._activeSession?.status === Status.Busy) { // If the session is busy, wait for it to become idle before @@ -753,34 +820,51 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { cancellable: false, }, async () => { await this.waitForIdle(); + this.markReady(); }); } else { // Enter the ready state immediately if the session is not busy - this._ready.open(); - this._state.fire(positron.RuntimeState.Ready); + this.markReady(); } } + + // If we don't have runtime info yet, get it now. + if (!runtimeInfo) { + runtimeInfo = await this.getKernelInfo(); + } + + return runtimeInfo; } /** - * Waits for the session to become idle before connecting. + * Waits for the session to become idle. * * @returns A promise that resolves when the session is idle. Does not time * out or reject. */ async waitForIdle(): Promise { - this.log(`Session ${this.metadata.sessionId} is busy; waiting for it to become idle before connecting.`, vscode.LogLevel.Info); return new Promise((resolve, _reject) => { this._state.event(async (state) => { if (state === positron.RuntimeState.Idle) { resolve(); - this._ready.open(); - this._state.fire(positron.RuntimeState.Ready); } }); }); } + /** + * Opens the ready barrier and fires the ready event. + */ + private markReady() { + // Open the ready barrier so that we can start sending messages + this._ready.open(); + + // Move into the ready state if we're not already there + if (this._runtimeState !== positron.RuntimeState.Ready) { + this.onStateChange(positron.RuntimeState.Ready); + } + } + /** * Connects or reconnects to the session's websocket. * @@ -880,7 +964,11 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { // Perform the restart this._restarting = true; try { + // Perform the restart on the server await this._api.restartSession(this.metadata.sessionId); + + // Mark ready after a successful restart + this.markReady(); } catch (err) { if (err instanceof HttpError) { throw new Error(err.body.message); @@ -965,6 +1053,23 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { if (data.hasOwnProperty('status')) { // Check to see if the status is a valid runtime state if (Object.values(positron.RuntimeState).includes(data.status)) { + // The 'starting' state typically follows 'uninitialized' (new + // session) or 'exited' (a restart). We can ignore the message + // in other cases as we've already broadcasted the state change + // to the client. + if (data.status === positron.RuntimeState.Starting && + this._runtimeState !== positron.RuntimeState.Uninitialized && + this._runtimeState !== positron.RuntimeState.Exited) { + this.log(`Ignoring 'starting' state message; already in state '${this._runtimeState}'`, vscode.LogLevel.Trace); + return; + } + // Same deal for 'ready' state; if we've already broadcasted the + // 'idle' state, ignore it. + if (data.status === positron.RuntimeState.Ready && + this._runtimeState === positron.RuntimeState.Idle) { + this.log(`Ignoring 'ready' state message; already in state '${this._runtimeState}'`, vscode.LogLevel.Trace); + return; + } this.onStateChange(data.status); } else { this.log(`Unknown state: ${data.status}`); @@ -987,7 +1092,7 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { private onStateChange(newState: positron.RuntimeState) { // If the kernel is ready, open the ready barrier if (newState === positron.RuntimeState.Ready) { - this.log(`Received initial heartbeat; kernel is ready.`); + this.log(`Kernel is ready.`); this._ready.open(); } this.log(`State: ${this._runtimeState} => ${newState}`, vscode.LogLevel.Debug); @@ -1004,11 +1109,17 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { this._connected.open(); } if (newState === positron.RuntimeState.Starting) { - this.log(`The kernel has started up after a restart.`, vscode.LogLevel.Info); - this._restarting = false; + if (this._restarting) { + this.log(`The kernel has started up after a restart.`, vscode.LogLevel.Info); + this._restarting = false; + } + } + + // Fire an event if the state has changed. + if (this._runtimeState !== newState) { + this._runtimeState = newState; + this._state.fire(newState); } - this._runtimeState = newState; - this._state.fire(newState); } /** @@ -1036,6 +1147,16 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { this._connected = new Barrier(); } + // All comms are now closed + this._comms.clear(); + + // Clear any pending requests + this._pendingRequests.clear(); + this._pendingUiCommRequests.forEach((req) => { + req.promise.reject(new Error('Kernel exited')); + }); + this._pendingUiCommRequests = []; + // We're no longer ready this._ready = new Barrier(); @@ -1068,13 +1189,39 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { // kernel to be connected. const request = new KernelInfoRequest(); const reply = await this.sendRequest(request); + return this.runtimeInfoFromKernelInfo(reply); + } + + /** + * Translates a kernel info reply into a runtime info object and updates the + * dynamic state. + * + * @param reply The Jupyter kernel info reply + * @returns The Positron runtime info object + */ + private runtimeInfoFromKernelInfo(reply: KernelInfoReply) { + // Read the input and continuation prompts + const input_prompt = reply.language_info.positron?.input_prompt; + const continuation_prompt = reply.language_info.positron?.continuation_prompt; + + // Populate the initial dynamic state with the input and continuation + // prompts + if (input_prompt) { + this.dynState.inputPrompt = input_prompt; + } + if (continuation_prompt) { + this.dynState.continuationPrompt = continuation_prompt; + } - // Translate the kernel info to a runtime info object + // Translate the kernel info into a runtime info object const info: positron.LanguageRuntimeInfo = { banner: reply.banner, implementation_version: reply.implementation_version, language_version: reply.language_info.version, + input_prompt, + continuation_prompt, }; + return info; } @@ -1209,11 +1356,12 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { * * @param logFile The path to the log file to stream */ - private streamLogFile(logFile: string) { + private async streamLogFile(logFile: string) { const logStreamer = new LogStreamer(this._kernelChannel, logFile, this.runtimeMetadata.languageName); + this._kernelChannel.appendLine(`Streaming kernel log file: ${logFile}`); this._disposables.push(logStreamer); this._kernelLogFile = logFile; - logStreamer.watch(); + return logStreamer.watch(); } /** @@ -1221,7 +1369,7 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { * * @param profileFilePath The path to the profile file to stream */ - private streamProfileFile(profileFilePath: string) { + private async streamProfileFile(profileFilePath: string) { this._profileChannel = positron.window.createRawLogOutputChannel( this.metadata.notebookUri ? @@ -1233,7 +1381,7 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { const profileStreamer = new LogStreamer(this._profileChannel, profileFilePath); this._disposables.push(profileStreamer); - profileStreamer.watch(); + await profileStreamer.watch(); } /** @@ -1257,11 +1405,49 @@ export class KallichoreSession implements JupyterLanguageRuntimeSession { case vscode.LogLevel.Info: this._consoleChannel.info(msg); break; + case vscode.LogLevel.Debug: + this._consoleChannel.debug(msg); + break; + case vscode.LogLevel.Trace: + this._consoleChannel.trace(msg); + break; default: this._consoleChannel.appendLine(msg); } } + /** + * Sends any pending messages to the UI comm. + * + * @param uiCommId The ID of the UI comm to send the messages to + */ + private async sendPendingUiCommRequests(uiCommId: string) { + // No work to do if there are no pending requests + if (this._pendingUiCommRequests.length === 0) { + return; + } + + // Move the pending requests to a local variable so we can clear the + // pending list and send the requests without worrying about reentrancy. + const pendingRequests = this._pendingUiCommRequests; + this._pendingUiCommRequests = []; + + // Wait for the kernel to be idle before sending any pending UI comm + // requests. + await this.waitForIdle(); + + const count = pendingRequests.length; + for (let i = 0; i < pendingRequests.length; i++) { + const req = pendingRequests[i]; + this.log(`Sending queued UI comm request '${req.method}' (${i + 1} of ${count})`, vscode.LogLevel.Debug); + try { + await this.performUiCommRequest(req, uiCommId); + } catch (err) { + this.log(`Failed to perform queued request '${req.method}': ${err}`, vscode.LogLevel.Error); + } + } + } + /** * Creates a short, unique ID. Use to help create unique identifiers for * comms, messages, etc. diff --git a/extensions/kallichore-adapter/src/LogStreamer.ts b/extensions/kallichore-adapter/src/LogStreamer.ts index c38041f8eb6..44e741c405e 100644 --- a/extensions/kallichore-adapter/src/LogStreamer.ts +++ b/extensions/kallichore-adapter/src/LogStreamer.ts @@ -26,15 +26,37 @@ export class LogStreamer implements vscode.Disposable { this._tail.on('error', (error) => this.appendLine(error)); } - public watch() { + /** + * Starts watching the log file. Waits up to 10 seconds for the log file to + * be created if it doesn't exist. + */ + public async watch() { + // Wait up to 10 seconds for the log file to be created. + for (let retry = 0; retry < 50; retry++) { + if (fs.existsSync(this._path)) { + break; + } else { + await new Promise((resolve) => setTimeout(resolve, 200)); + } + } + + if (!fs.existsSync(this._path)) { + this.appendLine(`Log file '${this._path}' not found after 10 seconds.`); + return; + } + // Initialise number of lines seen, which might not be zero as the // kernel might have already started outputting lines, or we might be // refreshing with an existing log file. This is used for flushing // the tail of the log on disposal. There is a race condition here so // this might be slightly off, causing duplicate lines in the tail of // the log. - const lines = fs.readFileSync(this._path, 'utf8').split('\n'); - this._linesCounter = lines.length; + try { + const lines = fs.readFileSync(this._path, 'utf8').split('\n'); + this._linesCounter = lines.length; + } catch (err) { + this.appendLine(`Error reading initial contents of log file '${this._path}': ${err.message || JSON.stringify(err)}`); + } // Start watching the log file. This streams output until the streamer is // disposed. diff --git a/extensions/kallichore-adapter/src/UICommRequest.ts b/extensions/kallichore-adapter/src/UICommRequest.ts new file mode 100644 index 00000000000..50bd2341ba9 --- /dev/null +++ b/extensions/kallichore-adapter/src/UICommRequest.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (C) 2024 Posit Software, PBC. All rights reserved. + * Licensed under the Elastic License 2.0. See LICENSE.txt for license information. + *--------------------------------------------------------------------------------------------*/ + +import { PromiseHandles } from './async'; + +/** + * A (possibly deferred) request to send a message to the UI comm. + */ +export class UICommRequest { + constructor( + public readonly method: string, + public readonly args: Array, + public readonly promise: PromiseHandles) { + } +} diff --git a/extensions/kallichore-adapter/src/jupyter/JupyterRequest.ts b/extensions/kallichore-adapter/src/jupyter/JupyterRequest.ts index e131c6b54c5..263fe9d6138 100644 --- a/extensions/kallichore-adapter/src/jupyter/JupyterRequest.ts +++ b/extensions/kallichore-adapter/src/jupyter/JupyterRequest.ts @@ -23,6 +23,10 @@ export abstract class JupyterRequest extends JupyterCommand { this._promise.resolve(response); } + public reject(reason: any): void { + this._promise.reject(reason); + } + public sendRpc(socket: SocketSession): Promise { super.sendCommand(socket); return this._promise.promise; diff --git a/extensions/positron-python/package.json b/extensions/positron-python/package.json index 3036c14d4c2..a9118acbb16 100644 --- a/extensions/positron-python/package.json +++ b/extensions/positron-python/package.json @@ -1916,8 +1916,5 @@ "webpack-require-from": "^1.8.6", "worker-loader": "^3.0.8", "yargs": "^15.3.1" - }, - "extensionDependencies": [ - "vscode.jupyter-adapter" - ] + } } diff --git a/extensions/positron-python/src/client/positron/session.ts b/extensions/positron-python/src/client/positron/session.ts index 121757bb051..246c5d5e8f4 100644 --- a/extensions/positron-python/src/client/positron/session.ts +++ b/extensions/positron-python/src/client/positron/session.ts @@ -434,8 +434,15 @@ export class PythonRuntimeSession implements positron.LanguageRuntimeSession, vs } private async createKernel(): Promise { - const config = vscode.workspace.getConfiguration('kallichoreSupervisor'); - const useKallichore = config.get('enable', false); + // Determine whether to use the Kallichore supervisor + let useKallichore = true; + if (vscode.env.uiKind === vscode.UIKind.Desktop) { + // In desktop mode, the supervisor is disabled by default, but can + // be enabled via the configuration. + const config = vscode.workspace.getConfiguration('kallichoreSupervisor'); + useKallichore = config.get('enable', false); + } + if (useKallichore) { // Use the Kallichore supervisor if enabled const ext = vscode.extensions.getExtension('vscode.kallichore-adapter'); diff --git a/extensions/positron-r/package.json b/extensions/positron-r/package.json index 9c676ce27b6..52bb11497b4 100644 --- a/extensions/positron-r/package.json +++ b/extensions/positron-r/package.json @@ -614,9 +614,6 @@ "lint": "eslint src --ext ts", "test": "node ./out/test/runTest.js" }, - "extensionDependencies": [ - "vscode.jupyter-adapter" - ], "devDependencies": { "@types/decompress": "^4.2.7", "@types/fs-extra": "^9.0.13", diff --git a/extensions/positron-r/src/session.ts b/extensions/positron-r/src/session.ts index c9f57cc2cbe..44ad6de2756 100644 --- a/extensions/positron-r/src/session.ts +++ b/extensions/positron-r/src/session.ts @@ -469,8 +469,15 @@ export class RSession implements positron.LanguageRuntimeSession, vscode.Disposa } private async createKernel(): Promise { - const config = vscode.workspace.getConfiguration('kallichoreSupervisor'); - const useKallichore = config.get('enable', false); + // Determine whether to use the Kallichore supervisor + let useKallichore = true; + if (vscode.env.uiKind === vscode.UIKind.Desktop) { + // In desktop mode, the supervisor is disabled by default, but can + // be enabled via the configuration. + const config = vscode.workspace.getConfiguration('kallichoreSupervisor'); + useKallichore = config.get('enable', false); + } + if (useKallichore) { // Use the Kallichore supervisor if enabled const ext = vscode.extensions.getExtension('vscode.kallichore-adapter'); diff --git a/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts b/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts index 4e46d11688e..75d376c0387 100644 --- a/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts +++ b/src/vs/workbench/services/runtimeSession/common/runtimeSession.ts @@ -922,6 +922,12 @@ export class RuntimeSessionService extends Disposable implements IRuntimeSession */ private attachToSession(session: ILanguageRuntimeSession, manager: ILanguageRuntimeSessionManager): void { + + // Ignore if already attached. + if (this._activeSessionsBySessionId.has(session.sessionId)) { + return; + } + // Save the session info. this._activeSessionsBySessionId.set(session.sessionId, new LanguageRuntimeSessionInfo(session, manager));