From d6e9e798e1d558220f22e8e4754c830582e39d06 Mon Sep 17 00:00:00 2001 From: Jonathan Date: Wed, 30 Oct 2024 15:44:13 -0700 Subject: [PATCH] Make the Jupyter supervisor the default in non-desktop Positron configurations (#5215) This change makes Kallichore the default in non-desktop configurations of Positron, such as on Posit Workbench. In addition to the configuration change, it includes a number of other changes that improve the stability of the supervisor integration in order to bring it up to the quality needed. In particular: - The supervisor's behavior around session starting has changed; the RPC that starts sessions now doesn't return until the session has fully started and returned kernel information. We pick up this kernel info in Positron rather than making our own RPC for kernel info (when starting for the first time). - We now hold any attempts to make UI Comm requests until the UI Comm has opened. This eliminates a lot of `setConsoleWidth` errors in the logs. - The Kallichore terminal is now hidden by default. - The R and Python extensions no longer have a hard dependency on the Jupyter Adapter extension. This allows them to work even when the Jupyter Adapter is unbootable (such as on REHL 9 or other systems where there are glibc issues with ZeroMQ) Addresses #4941. ### QA Notes If logging issues, it's helpful to include the Kallichore logs, so run with the "Show Terminal" setting (added in this PR) turned on. --------- Signed-off-by: Jonathan Co-authored-by: sharon --- extensions/kallichore-adapter/package.json | 7 +- .../kallichore-adapter/package.nls.json | 3 +- .../src/KallichoreAdapterApi.ts | 24 +- .../src/KallichoreSession.ts | 268 +++++++++++++++--- .../kallichore-adapter/src/LogStreamer.ts | 28 +- .../kallichore-adapter/src/UICommRequest.ts | 17 ++ .../src/jupyter/JupyterRequest.ts | 4 + extensions/positron-python/package.json | 5 +- .../src/client/positron/session.ts | 11 +- extensions/positron-r/package.json | 3 - extensions/positron-r/src/session.ts | 11 +- .../runtimeSession/common/runtimeSession.ts | 6 + 12 files changed, 328 insertions(+), 59 deletions(-) create mode 100644 extensions/kallichore-adapter/src/UICommRequest.ts 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));