Skip to content

Commit

Permalink
Add option to leave kernel sessions running when Positron is closed (#…
Browse files Browse the repository at this point in the history
…5899)

This change makes it possible to close Positron but leave Python or R
running, to be resumed/reconnected when Positron is opened again. A new
setting controls this behavior; it lets you specify how long to let
sessions run idle in the background before they are automatically
closed.

<img width="734" alt="image"
src="https://github.com/user-attachments/assets/88b1dce9-fdfd-4cef-949e-1244c2ba94ea"
/>

- Enables long-running remote kernels that can be accessed over SSH
- For advanced users, enables faster startup and the ability to safely
close Positron Desktop without losing data or interrupting computations
- Addresses an issue with accumulated orphaned supervisor process
observed in some dev environments; these processes will now clean
themselves up after an hour
- Enables testing of various "leave and come back later" scenarios on
Positron Desktop that could formerly only be tested in server and/or
Workbench configurations

### How it Works

1. When sessions are not specified to be closed when Positron is closed,
the kernel supervisor is:
1. started with a new `--idle-shutdown-hours` flag so that it shuts down
on its own after all sessions have been idle for the specified number of
hours
2. started with `nohup` (Unix-alike) or `start /b` (Windows) so that it
continues running outside the terminal host after Positron closes
2. Persistent sessions are given a new `SessionLocation`: `Machine`
3. Positron saves information about persistent sessions to durable
workspace storage rather than ephemeral storage
4. At startup, Positron checks all the persistent sessions to see if
they are still valid (i.e. are still running). It reconnects to any that
are, in the same way that it would reconnect to a session after a
reload.

Note that all of this new behavior is opt-in; if the setting is left at
its default, Positron behaves the same way it does today.

### Release Notes

#### New Features

- Option to leave Python or R running when Positron is closed, for
remote sessions or long-running computations (#4996)

#### Bug Fixes

- N/A

### QA Notes

- This change should not impact anything when the new setting is left at
its default value.
- If the kernel session exits while Positron is closed, Positron should
not barf when it is reopened; instead, it should just start a new kernel
session.
- You really do need a full Positron restart when turning this setting
on. Once on, you will find that Positron continues to connect to the
persistent sessions until they time out or exited, even after turning
the setting off, since the setting only applies to new sessions created
in the new Positron window after changing the setting.
- Some test speedup may be possible with this setting since it lets you
repeatedly open and close Positron without waiting for runtime startup.
(The downside, of course, is that subsequent opens aren't starting with
a clean slate, which may or may not be important).
- On Remote SSH, sessions will only persist when started inside a
folder/workspace as the blank/empty workspace is effectively re-created
every time you use it.

---------

Signed-off-by: Jonathan <[email protected]>
Co-authored-by: sharon <[email protected]>
  • Loading branch information
jmcphers and sharon-wang authored Jan 10, 2025
1 parent 11d5ddf commit 968a8ac
Show file tree
Hide file tree
Showing 21 changed files with 497 additions and 45 deletions.
5 changes: 5 additions & 0 deletions extensions/positron-python/src/client/jupyter-adapter.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ export interface JupyterAdapterApi extends vscode.Disposable {
extra?: JupyterKernelExtra | undefined,
): Promise<JupyterLanguageRuntimeSession>;

/**
* Validate an existing session for a Jupyter-compatible kernel.
*/
validateSession(sessionId: string): Promise<boolean>;

/**
* Restore a session for a Jupyter-compatible kernel.
*
Expand Down
27 changes: 26 additions & 1 deletion extensions/positron-python/src/client/positron/manager.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved.
* Copyright (C) 2023-2025 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable global-require */
/* eslint-disable class-methods-use-this */
import * as portfinder from 'portfinder';
// eslint-disable-next-line import/no-unresolved
import * as positron from 'positron';
import * as vscode from 'vscode';
import * as path from 'path';
import * as os from 'os';

Expand Down Expand Up @@ -254,6 +255,30 @@ export class PythonRuntimeManager implements IPythonRuntimeManager {
return registeredMetadata ?? metadata;
}

/**
* Validate an existing session for a Jupyter-compatible kernel.
*
* @param sessionId The session ID to validate
* @returns True if the session is valid, false otherwise
*/
async validateSession(sessionId: string): Promise<boolean> {
const config = vscode.workspace.getConfiguration('kernelSupervisor');
if (config.get<boolean>('enable', true)) {
const ext = vscode.extensions.getExtension('positron.positron-supervisor');
if (!ext) {
throw new Error('Positron Supervisor extension not found');
}
if (!ext.isActive) {
await ext.activate();
}
return ext.exports.validateSession(sessionId);
}

// When not using the kernel supervisor, sessions are not
// persisted.
return false;
}

/**
* Wrapper for Python runtime discovery method that caches the metadata
* before it's returned to Positron.
Expand Down
14 changes: 12 additions & 2 deletions extensions/positron-python/src/client/positron/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2024 Posit Software, PBC. All rights reserved.
* Copyright (C) 2024-2025 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable global-require */
// eslint-disable-next-line import/no-unresolved
import * as positron from 'positron';
import * as vscode from 'vscode';
import * as fs from 'fs-extra';
import * as os from 'os';
import * as path from 'path';
Expand Down Expand Up @@ -104,6 +105,15 @@ export async function createPythonRuntimeMetadata(
pythonEnvironmentId: interpreter.id || '',
};

// Check the kernel supervisor's configuration; if it's enabled and
// configured to persist sessions, mark the session location as 'machine'
// so that Positron will reattach to the session after Positron is reopened.
const config = vscode.workspace.getConfiguration('kernelSupervisor');
const sessionLocation =
config.get<boolean>('enable', true) && config.get<string>('shutdownTimeout', 'immediately') !== 'immediately'
? positron.LanguageRuntimeSessionLocation.Machine
: positron.LanguageRuntimeSessionLocation.Workspace;

// Create the metadata for the language runtime
const metadata: positron.LanguageRuntimeMetadata = {
runtimeId,
Expand All @@ -119,7 +129,7 @@ export async function createPythonRuntimeMetadata(
.readFileSync(path.join(EXTENSION_ROOT_DIR, 'resources', 'branding', 'python-icon.svg'))
.toString('base64'),
startupBehavior,
sessionLocation: positron.LanguageRuntimeSessionLocation.Workspace,
sessionLocation,
extraRuntimeData,
};

Expand Down
8 changes: 6 additions & 2 deletions extensions/positron-r/src/jupyter-adapter.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2023-2024 Posit Software, PBC. All rights reserved.
* Copyright (C) 2023-2025 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';

// eslint-disable-next-line import/no-unresolved
import * as positron from 'positron';

export interface JupyterSessionState {
Expand Down Expand Up @@ -149,6 +148,11 @@ export interface JupyterAdapterApi extends vscode.Disposable {
extra?: JupyterKernelExtra | undefined,
): Promise<JupyterLanguageRuntimeSession>;

/**
* Validate an existing session for a Jupyter-compatible kernel.
*/
validateSession(sessionId: string): Promise<boolean>;

/**
* Restore a session for a Jupyter-compatible kernel.
*
Expand Down
10 changes: 9 additions & 1 deletion extensions/positron-r/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,14 @@ export async function makeMetadata(
reasonDiscovered: rInst.reasonDiscovered,
};

// Check the kernel supervisor's configuration; if it's enabled and
// configured to persist sessions, mark the session location as 'machine'
// so that Positron will reattach to the session after Positron is reopened.
const config = vscode.workspace.getConfiguration('kernelSupervisor');
const sessionLocation = config.get<boolean>('enable', true) &&
config.get<string>('shutdownTimeout', 'immediately') !== 'immediately' ?
positron.LanguageRuntimeSessionLocation.Machine : positron.LanguageRuntimeSessionLocation.Workspace;

const metadata: positron.LanguageRuntimeMetadata = {
runtimeId,
runtimeName,
Expand All @@ -230,7 +238,7 @@ export async function makeMetadata(
fs.readFileSync(
path.join(EXTENSION_ROOT_DIR, 'resources', 'branding', 'r-icon.svg')
).toString('base64'),
sessionLocation: positron.LanguageRuntimeSessionLocation.Workspace,
sessionLocation,
startupBehavior,
extraRuntimeData
};
Expand Down
26 changes: 25 additions & 1 deletion extensions/positron-r/src/runtime-manager.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2024 Posit Software, PBC. All rights reserved.
* Copyright (C) 2024-2025 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

Expand Down Expand Up @@ -88,6 +88,30 @@ export class RRuntimeManager implements positron.LanguageRuntimeManager {
return Promise.resolve(makeMetadata(inst, positron.LanguageRuntimeStartupBehavior.Immediate));
}

/**
* Validate an existing session for a Jupyter-compatible kernel.
*
* @param sessionId The session ID to validate
* @returns True if the session is valid, false otherwise
*/
async validateSession(sessionId: string): Promise<boolean> {
const config = vscode.workspace.getConfiguration('kernelSupervisor');
if (config.get<boolean>('enable', true)) {
const ext = vscode.extensions.getExtension('positron.positron-supervisor');
if (!ext) {
throw new Error('Positron Supervisor extension not found');
}
if (!ext.isActive) {
await ext.activate();
}
return ext.exports.validateSession(sessionId);
}

// When not using the kernel supervisor, sessions are not
// persisted.
return false;
}

restoreSession(
runtimeMetadata: positron.LanguageRuntimeMetadata,
sessionMetadata: positron.RuntimeSessionMetadata): Thenable<positron.LanguageRuntimeSession> {
Expand Down
8 changes: 8 additions & 0 deletions extensions/positron-reticulate/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,14 @@ class ReticulateRuntimeMetadata implements positron.LanguageRuntimeMetadata {
path.join(CONTEXT.extensionPath, 'resources', 'branding', 'reticulate.svg'),
{ encoding: 'base64' }
);
// Check the kernel supervisor's configuration; if it's enabled and
// configured to persist sessions, mark the session location as 'machine'
// so that Positron will reattach to the session after Positron is reopened.
const config = vscode.workspace.getConfiguration('kernelSupervisor');
this.sessionLocation = config.get<boolean>('enable', true) &&
config.get<string>('shutdownTimeout', 'immediately') !== 'immediately' ?
positron.LanguageRuntimeSessionLocation.Machine : positron.LanguageRuntimeSessionLocation.Workspace;

}
runtimePath: string = 'Managed by the reticulate package';
runtimeName: string = 'Python (reticulate)';
Expand Down
28 changes: 27 additions & 1 deletion extensions/positron-supervisor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,32 @@
"default": "debug",
"description": "%configuration.logLevel.description%"
},
"kernelSupervisor.shutdownTimeout": {
"scope": "window",
"type": "string",
"enum": [
"immediately",
"when idle",
"4",
"8",
"12",
"24",
"168",
"indefinitely"
],
"enumDescriptions": [
"%configuration.shutdownTimeout.immediately.description%",
"%configuration.shutdownTimeout.whenIdle.description%",
"%configuration.shutdownTimeout.4.description%",
"%configuration.shutdownTimeout.8.description%",
"%configuration.shutdownTimeout.12.description%",
"%configuration.shutdownTimeout.24.description%",
"%configuration.shutdownTimeout.168.description%",
"%configuration.shutdownTimeout.indefinitely.description%"
],
"default": "immediately",
"markdownDescription": "%configuration.shutdownTimeout.description%"
},
"kernelSupervisor.attachOnStartup": {
"scope": "window",
"type": "boolean",
Expand Down Expand Up @@ -115,7 +141,7 @@
},
"positron": {
"binaryDependencies": {
"kallichore": "0.1.26"
"kallichore": "0.1.27"
}
},
"dependencies": {
Expand Down
9 changes: 9 additions & 0 deletions extensions/positron-supervisor/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@
"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.shutdownTimeout.description": "When should kernels be shut down after Positron is closed?\n\nTimeouts are in hours and start once the kernel is idle. Restart Positron to apply.",
"configuration.shutdownTimeout.immediately.description": "Shut down kernels immediately when Positron is closed",
"configuration.shutdownTimeout.whenIdle.description": "Wait for kernels to finish any running computations, then shut them down",
"configuration.shutdownTimeout.4.description": "After idle for 4 hours",
"configuration.shutdownTimeout.8.description": "After idle for 8 hours",
"configuration.shutdownTimeout.12.description": "After idle for 12 hours",
"configuration.shutdownTimeout.24.description": "After idle for 1 day",
"configuration.shutdownTimeout.168.description": "After idle for 7 days",
"configuration.shutdownTimeout.indefinitely.description": "Leave kernels running indefinitely",
"configuration.enable.description": "Run Jupyter kernels under the Positron kernel supervisor.",
"configuration.showTerminal.description": "Show the host terminal for the Positron kernel supervisor",
"configuration.connectionTimeout.description": "Timeout in seconds for connecting to the kernel's sockets",
Expand Down
Loading

0 comments on commit 968a8ac

Please sign in to comment.