Skip to content

Commit 1a9c48a

Browse files
committed
Apply suggestions from code review
1 parent 96b3612 commit 1a9c48a

1 file changed

Lines changed: 61 additions & 30 deletions

File tree

ggsql-vscode/src/manager.ts

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,22 @@ import * as vscode from 'vscode';
99
import * as fs from 'fs';
1010
import * as path from 'path';
1111
import * as cp from 'child_process';
12+
import * as crypto from 'crypto';
1213
import type * as positron from '@posit-dev/positron';
1314
import type { JupyterKernelSpec, JupyterSession, JupyterKernel, PositronSupervisorApi } from './types';
1415
import { log } from './extension';
1516

17+
/** Where a kernel candidate was discovered */
18+
type KernelSource = 'Setting' | 'Jupyter' | 'System' | 'Path';
19+
1620
/**
1721
* A discovered ggsql-jupyter kernel candidate
1822
*/
1923
interface KernelCandidate {
2024
/** Absolute path to the ggsql-jupyter binary (or bare name for PATH fallback) */
2125
kernelPath: string;
2226
/** Human-readable label for where this was found */
23-
source: string;
27+
source: KernelSource;
2428
}
2529

2630
/**
@@ -42,7 +46,7 @@ function discoverKernelPaths(): KernelCandidate[] {
4246
const config = vscode.workspace.getConfiguration('ggsql');
4347
const configuredPath = config.get<string>('kernelPath', '');
4448
if (configuredPath && configuredPath.trim() !== '') {
45-
candidates.push({ kernelPath: configuredPath, source: 'setting' });
49+
candidates.push({ kernelPath: configuredPath, source: 'Setting' });
4650
}
4751

4852
// 2. Jupyter kernelspec locations
@@ -52,6 +56,11 @@ function discoverKernelPaths(): KernelCandidate[] {
5256
path.join(homeDir, 'Library', 'Jupyter', 'kernels', 'ggsql', binaryName),
5357
// User kernelspec (Linux)
5458
path.join(homeDir, '.local', 'share', 'jupyter', 'kernels', 'ggsql', binaryName),
59+
// User kernelspec (Windows)
60+
path.join(
61+
process.env.APPDATA || path.join(homeDir, 'AppData', 'Roaming'),
62+
'jupyter', 'kernels', 'ggsql', binaryName
63+
),
5564
// System kernelspec (macOS)
5665
path.join('/usr', 'local', 'share', 'jupyter', 'kernels', 'ggsql', binaryName),
5766
// System kernelspec (Linux)
@@ -137,15 +146,15 @@ async function isKernelAccessible(kernelPath: string): Promise<boolean> {
137146
function generateMetadata(
138147
context: vscode.ExtensionContext,
139148
candidate: KernelCandidate,
140-
index: number
141149
): positron.LanguageRuntimeMetadata {
142150
const version = context.extension.packageJSON.version as string;
143151

144152
const iconPath = path.join(context.extensionPath, 'resources', 'ggsql-icon.svg');
145153
const base64Icon = fs.readFileSync(iconPath).toString('base64');
146154

155+
const pathHash = crypto.createHash('sha256').update(candidate.kernelPath).digest('hex').substring(0, 12);
147156
return {
148-
runtimeId: index === 0 ? 'ggsql-jupyter' : `ggsql-jupyter-${index}`,
157+
runtimeId: `ggsql-${pathHash}`,
149158
runtimePath: candidate.kernelPath,
150159
runtimeName: `ggsql (${candidate.source})`,
151160
runtimeShortName: 'ggsql',
@@ -284,7 +293,7 @@ function writeKernelJson(kernelDir: string, kernelPath: string): void {
284293
argv: [kernelPath, '-f', '{connection_file}'],
285294
display_name: 'ggsql',
286295
language: 'ggsql',
287-
interrupt_mode: 'message',
296+
interrupt_mode: 'signal',
288297
env: { RUST_LOG: 'error' },
289298
metadata: { debugger: false }
290299
};
@@ -308,16 +317,49 @@ function writeKernelJson(kernelDir: string, kernelPath: string): void {
308317
}
309318

310319
/**
311-
* Install a Jupyter kernel spec for ggsql so that external tools like Quarto
312-
* can discover it via `jupyter kernelspec list`.
313-
*
314-
* Writes kernel.json to the appropriate Jupyter kernelspec directory: the
315-
* active virtualenv/conda env if detected, otherwise the user-level dir.
320+
* Try to resolve a binary name to its absolute path via the system PATH.
321+
* Returns the original value if resolution fails.
322+
*/
323+
function resolveKernelPath(kernelPath: string): string {
324+
if (path.isAbsolute(kernelPath)) {
325+
return kernelPath;
326+
}
327+
try {
328+
const cmd = process.platform === 'win32' ? 'where' : 'which';
329+
const resolved = cp.execFileSync(cmd, [kernelPath], {
330+
encoding: 'utf8',
331+
timeout: 5000,
332+
}).trim().split(/\r?\n/)[0];
333+
if (resolved && path.isAbsolute(resolved)) {
334+
log(`Resolved '${kernelPath}' to '${resolved}'`);
335+
return resolved;
336+
}
337+
} catch {
338+
log(`Could not resolve '${kernelPath}' to absolute path, using as-is`);
339+
}
340+
return kernelPath;
341+
}
342+
343+
/**
344+
* Ensure a Jupyter kernel spec is installed so that external tools like
345+
* Quarto can discover ggsql. Called from session creation/restoration.
316346
*
317-
* @param kernelPath Absolute path to the ggsql-jupyter binary
347+
* Writes to the active virtualenv/conda env if detected, otherwise the
348+
* user-level kernelspec directory.
349+
*/
350+
function ensureKernelSpecInstalled(kernelPath: string): void {
351+
writeKernelJson(getJupyterKernelDir(), resolveKernelPath(kernelPath));
352+
}
353+
354+
/**
355+
* Create the dynamic state for a ggsql runtime session.
318356
*/
319-
function installJupyterKernelSpec(kernelPath: string): void {
320-
writeKernelJson(getJupyterKernelDir(), kernelPath);
357+
function createDynState(): positron.LanguageRuntimeDynState {
358+
return {
359+
inputPrompt: 'ggsql> ',
360+
continuationPrompt: '... ',
361+
sessionName: 'ggsql',
362+
};
321363
}
322364

323365
/**
@@ -347,21 +389,19 @@ export class GgsqlRuntimeManager implements positron.LanguageRuntimeManager {
347389
const candidates = discoverKernelPaths();
348390
log(`Found ${candidates.length} kernel candidate(s)`);
349391

350-
let index = 0;
351392
for (const candidate of candidates) {
352393
const accessible = await isKernelAccessible(candidate.kernelPath);
353394
if (accessible) {
354395
// When a system install is found, write the kernel spec to
355396
// the user kernelspec dir immediately so that Quarto/Jupyter
356397
// can discover ggsql even if no session is ever started.
357398
if (candidate.source === 'System') {
358-
writeKernelJson(getUserJupyterKernelDir(), candidate.kernelPath);
399+
writeKernelJson(getUserJupyterKernelDir(), resolveKernelPath(candidate.kernelPath));
359400
}
360401

361-
const metadata = generateMetadata(context, candidate, index);
402+
const metadata = generateMetadata(context, candidate);
362403
log(`Yielding runtime: ${metadata.runtimeName} (${metadata.runtimeId}) at ${candidate.kernelPath}`);
363404
yield metadata;
364-
index++;
365405
} else {
366406
log(`Skipping inaccessible kernel: ${candidate.kernelPath}`);
367407
}
@@ -408,15 +448,10 @@ export class GgsqlRuntimeManager implements positron.LanguageRuntimeManager {
408448
// Create the kernel spec using the runtime's kernel path
409449
const kernelSpec = createKernelSpec(runtimeMetadata.runtimePath, workspacePath);
410450

411-
// Create the dynamic state
412-
const dynState: positron.LanguageRuntimeDynState = {
413-
inputPrompt: 'ggsql> ',
414-
continuationPrompt: '... ',
415-
sessionName: 'ggsql'
416-
};
451+
const dynState = createDynState();
417452

418453
// Advertise this kernel to external tools (Quarto, Jupyter)
419-
installJupyterKernelSpec(runtimeMetadata.runtimePath);
454+
ensureKernelSpecInstalled(runtimeMetadata.runtimePath);
420455

421456
// Create the session using the supervisor
422457
const session = await supervisorApi.createSession(
@@ -455,14 +490,10 @@ export class GgsqlRuntimeManager implements positron.LanguageRuntimeManager {
455490

456491
const supervisorApi = await supervisorExt.activate();
457492

458-
const dynState: positron.LanguageRuntimeDynState = {
459-
inputPrompt: 'ggsql> ',
460-
continuationPrompt: '... ',
461-
sessionName: 'ggsql'
462-
};
493+
const dynState = createDynState();
463494

464495
// Re-advertise this kernel on restore
465-
installJupyterKernelSpec(runtimeMetadata.runtimePath);
496+
ensureKernelSpecInstalled(runtimeMetadata.runtimePath);
466497

467498
const session = await supervisorApi.restoreSession(
468499
runtimeMetadata,

0 commit comments

Comments
 (0)