|
1 | 1 | import { createLogger } from "@runt/lib"; |
2 | 2 | import * as path from "@std/path"; |
| 3 | +import * as fs from "jsr:@std/fs"; |
| 4 | +import jmp from "@runtimed/jmp"; |
3 | 5 |
|
4 | 6 | export class PythonWorker { |
5 | 7 | private kernel: Deno.ChildProcess | null = null; |
@@ -68,4 +70,111 @@ export class PythonWorker { |
68 | 70 | getConnPath(): string | null { |
69 | 71 | return this.connPath; |
70 | 72 | } |
| 73 | + |
| 74 | + /** |
| 75 | + * Returns a JupyterKernelConnection instance after the kernel is started. |
| 76 | + */ |
| 77 | + async getConnection(): Promise<JupyterKernelConnection> { |
| 78 | + if (!this.connPath) throw new Error("Kernel not started"); |
| 79 | + return await JupyterKernelConnection.connect(this.connPath); |
| 80 | + } |
| 81 | +} |
| 82 | + |
| 83 | +/** |
| 84 | + * Connects to a Jupyter kernel using conn.json and @runtimed/jmp. |
| 85 | + */ |
| 86 | +export class JupyterKernelConnection { |
| 87 | + private shellSocket: any; |
| 88 | + private iopubSocket: any; |
| 89 | + private controlSocket: any; |
| 90 | + private stdinSocket: any; |
| 91 | + private hbSocket: any; |
| 92 | + private config: any; |
| 93 | + private session: string; |
| 94 | + private key: string; |
| 95 | + private scheme: string; |
| 96 | + private username: string; |
| 97 | + |
| 98 | + private constructor(config: any, sockets: Record<string, any>) { |
| 99 | + this.config = config; |
| 100 | + this.shellSocket = sockets.shell; |
| 101 | + this.iopubSocket = sockets.iopub; |
| 102 | + this.controlSocket = sockets.control; |
| 103 | + this.stdinSocket = sockets.stdin; |
| 104 | + this.hbSocket = sockets.hb; |
| 105 | + this.session = crypto.randomUUID(); |
| 106 | + this.key = config.key; |
| 107 | + this.scheme = config.signature_scheme.replace("hmac-", ""); |
| 108 | + this.username = "runt"; |
| 109 | + } |
| 110 | + |
| 111 | + static async connect(connPath: string): Promise<JupyterKernelConnection> { |
| 112 | + const configRaw = await Deno.readTextFile(connPath); |
| 113 | + const config = JSON.parse(configRaw); |
| 114 | + const mkAddr = (port: number) => `${config.transport}://${config.ip}:${port}`; |
| 115 | + const scheme = config.signature_scheme.replace("hmac-", ""); |
| 116 | + const key = config.key; |
| 117 | + const shell = new jmp.Socket("dealer", scheme, key); |
| 118 | + const iopub = new jmp.Socket("sub", scheme, key); |
| 119 | + const control = new jmp.Socket("dealer", scheme, key); |
| 120 | + const stdin = new jmp.Socket("dealer", scheme, key); |
| 121 | + const hb = new jmp.Socket("req", scheme, key); |
| 122 | + shell.connect(mkAddr(config.shell_port)); |
| 123 | + iopub.connect(mkAddr(config.iopub_port)); |
| 124 | + iopub.subscribe(""); |
| 125 | + control.connect(mkAddr(config.control_port)); |
| 126 | + stdin.connect(mkAddr(config.stdin_port)); |
| 127 | + hb.connect(mkAddr(config.hb_port)); |
| 128 | + return new JupyterKernelConnection(config, { shell, iopub, control, stdin, hb }); |
| 129 | + } |
| 130 | + |
| 131 | + /** |
| 132 | + * Execute Python code and return the result as a Promise. |
| 133 | + */ |
| 134 | + async execute(code: string): Promise<{ result: string; outputs: unknown[] }> { |
| 135 | + const msg_id = crypto.randomUUID(); |
| 136 | + const header = { |
| 137 | + msg_id, |
| 138 | + username: this.username, |
| 139 | + session: this.session, |
| 140 | + msg_type: "execute_request", |
| 141 | + version: "5.3", |
| 142 | + }; |
| 143 | + const content = { |
| 144 | + code, |
| 145 | + silent: false, |
| 146 | + store_history: true, |
| 147 | + user_expressions: {}, |
| 148 | + allow_stdin: false, |
| 149 | + stop_on_error: true, |
| 150 | + }; |
| 151 | + const msg = new jmp.Message(); |
| 152 | + msg.header = header; |
| 153 | + msg.parent_header = {}; |
| 154 | + msg.metadata = {}; |
| 155 | + msg.content = content; |
| 156 | + msg.idents = []; |
| 157 | + // Send execute_request |
| 158 | + this.shellSocket.send(msg); |
| 159 | + // Listen for iopub messages for this msg_id |
| 160 | + const outputs: unknown[] = []; |
| 161 | + let result: string | undefined; |
| 162 | + for await (const msg of this.iopubSocket) { |
| 163 | + if (msg.parent_header?.msg_id !== msg_id) continue; |
| 164 | + if (msg.header.msg_type === "execute_result" || msg.header.msg_type === "display_data") { |
| 165 | + outputs.push(msg.content); |
| 166 | + if (msg.content.data && msg.content.data["text/plain"]) { |
| 167 | + result = msg.content.data["text/plain"]; |
| 168 | + } |
| 169 | + } else if (msg.header.msg_type === "stream") { |
| 170 | + outputs.push(msg.content); |
| 171 | + } else if (msg.header.msg_type === "error") { |
| 172 | + outputs.push(msg.content); |
| 173 | + result = msg.content.ename + ": " + msg.content.evalue; |
| 174 | + } else if (msg.header.msg_type === "status" && msg.content.execution_state === "idle") { |
| 175 | + break; |
| 176 | + } |
| 177 | + } |
| 178 | + return { result: result ?? "", outputs }; |
| 179 | + } |
71 | 180 | } |
0 commit comments