mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
[web] include output tracking in run-finished lifecycle event in pyodide
GitOrigin-RevId: 5c72abc35ea4045f9c6aa374a2c51490f8f6cd38
This commit is contained in:
committed by
Copybot
parent
b893ba36e2
commit
7d032e73d6
@@ -14,7 +14,7 @@ export type LifecycleCallback = (
|
|||||||
event:
|
event:
|
||||||
| { type: 'loaded' }
|
| { type: 'loaded' }
|
||||||
| { type: 'loading-failed'; error: string }
|
| { type: 'loading-failed'; error: string }
|
||||||
| { type: 'run-finished'; requestId: string }
|
| { type: 'run-finished'; requestId: string; outputs: string[] }
|
||||||
) => void
|
) => void
|
||||||
|
|
||||||
export class PyodideWorkerClient {
|
export class PyodideWorkerClient {
|
||||||
@@ -172,6 +172,7 @@ export class PyodideWorkerClient {
|
|||||||
this.lifecycleCallback?.({
|
this.lifecycleCallback?.({
|
||||||
type: 'run-finished',
|
type: 'run-finished',
|
||||||
requestId: response.id,
|
requestId: response.id,
|
||||||
|
outputs: response.outputs,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export type PyodideWorkerEvent =
|
|||||||
export type RunCodeResult = {
|
export type RunCodeResult = {
|
||||||
type: 'run-code-result'
|
type: 'run-code-result'
|
||||||
id: string
|
id: string
|
||||||
|
outputs: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PyodideWorkerResponse = PyodideWorkerEvent | RunCodeResult
|
export type PyodideWorkerResponse = PyodideWorkerEvent | RunCodeResult
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ type PyodideFS = PyodideInterface['FS']
|
|||||||
type PyodideModule = typeof import('pyodide')
|
type PyodideModule = typeof import('pyodide')
|
||||||
|
|
||||||
const PROJECT_FS_ROOT = '/project'
|
const PROJECT_FS_ROOT = '/project'
|
||||||
|
const PROJECT_FS_PREFIX = `${PROJECT_FS_ROOT}/`
|
||||||
const PYODIDE_INDEX_PATH = 'js/libs/pyodide/'
|
const PYODIDE_INDEX_PATH = 'js/libs/pyodide/'
|
||||||
|
|
||||||
function ensureDirectoryExists(fs: PyodideFS, filePath: string) {
|
function ensureDirectoryExists(fs: PyodideFS, filePath: string) {
|
||||||
@@ -94,6 +95,7 @@ async function handleRunCode(msg: RunCodeRequest) {
|
|||||||
self.postMessage({
|
self.postMessage({
|
||||||
type: 'run-code-result',
|
type: 'run-code-result',
|
||||||
id: msg.id,
|
id: msg.id,
|
||||||
|
outputs: [],
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -102,6 +104,8 @@ async function handleRunCode(msg: RunCodeRequest) {
|
|||||||
env: { MPLBACKEND: 'Agg' },
|
env: { MPLBACKEND: 'Agg' },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const writtenPaths = new Set<string>()
|
||||||
|
|
||||||
instance.setStdout({
|
instance.setStdout({
|
||||||
batched: (line: string) => {
|
batched: (line: string) => {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
@@ -124,11 +128,25 @@ async function handleRunCode(msg: RunCodeRequest) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const fs = instance.FS
|
||||||
if (msg.files.length > 0) {
|
if (msg.files.length > 0) {
|
||||||
const fs = instance.FS
|
|
||||||
syncProjectFiles(fs, msg.files)
|
syncProjectFiles(fs, msg.files)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const originalWrite = fs.write as PyodideFS['write']
|
||||||
|
fs.write = ((...args: Parameters<PyodideFS['write']>) => {
|
||||||
|
const [stream] = args
|
||||||
|
// Only surface writes to the synced project workspace, not Pyodide internals.
|
||||||
|
if (
|
||||||
|
typeof stream?.path === 'string' &&
|
||||||
|
stream.path.startsWith(PROJECT_FS_PREFIX)
|
||||||
|
) {
|
||||||
|
writtenPaths.add(stream.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return originalWrite(...args)
|
||||||
|
}) as PyodideFS['write']
|
||||||
|
|
||||||
const result = await instance.runPythonAsync(msg.code)
|
const result = await instance.runPythonAsync(msg.code)
|
||||||
if (result !== undefined) {
|
if (result !== undefined) {
|
||||||
self.postMessage({
|
self.postMessage({
|
||||||
@@ -148,12 +166,14 @@ async function handleRunCode(msg: RunCodeRequest) {
|
|||||||
line: errorMessage,
|
line: errorMessage,
|
||||||
requestId: msg.id,
|
requestId: msg.id,
|
||||||
})
|
})
|
||||||
|
} finally {
|
||||||
|
const outputs = [...writtenPaths].sort()
|
||||||
|
self.postMessage({
|
||||||
|
type: 'run-code-result',
|
||||||
|
id: msg.id,
|
||||||
|
outputs,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
self.postMessage({
|
|
||||||
type: 'run-code-result',
|
|
||||||
id: msg.id,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.addEventListener('message', async event => {
|
self.addEventListener('message', async event => {
|
||||||
|
|||||||
@@ -97,25 +97,70 @@ describe('PyodideWorkerClient', function () {
|
|||||||
expect(runRequest).to.include({ type: 'run-code', id: 'boom.py' })
|
expect(runRequest).to.include({ type: 'run-code', id: 'boom.py' })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('emits run-finished lifecycle event from run-code-result', function () {
|
function setupClientWithLifecycleTracking() {
|
||||||
const client = new PyodideWorkerClient({ baseAssetPath: BASE_ASSET_PATH })
|
const client = new PyodideWorkerClient({ baseAssetPath: BASE_ASSET_PATH })
|
||||||
const worker = WorkerMock.instances[0]
|
const worker = WorkerMock.instances[0]
|
||||||
const lifecycleEvents: Array<{ type: string; requestId?: string }> = []
|
const lifecycleEvents: Array<{
|
||||||
|
type: string
|
||||||
client.setLifecycleCallback(event => {
|
requestId?: string
|
||||||
lifecycleEvents.push(event)
|
outputs?: string[]
|
||||||
})
|
}> = []
|
||||||
|
client.setLifecycleCallback(event => lifecycleEvents.push(event))
|
||||||
worker.emitMessage({ type: 'listening' })
|
worker.emitMessage({ type: 'listening' })
|
||||||
|
return { client, worker, lifecycleEvents }
|
||||||
|
}
|
||||||
|
|
||||||
|
it('emits run-finished lifecycle event from run-code-result', function () {
|
||||||
|
const { client, worker, lifecycleEvents } =
|
||||||
|
setupClientWithLifecycleTracking()
|
||||||
|
|
||||||
client.runCode('print("ok")', { requestId: 'main.py', files: [] })
|
client.runCode('print("ok")', { requestId: 'main.py', files: [] })
|
||||||
worker.emitMessage({
|
worker.emitMessage({
|
||||||
type: 'run-code-result',
|
type: 'run-code-result',
|
||||||
id: 'main.py',
|
id: 'main.py',
|
||||||
|
outputs: ['/project/output.txt'],
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(lifecycleEvents).to.deep.include({
|
expect(lifecycleEvents).to.deep.include({
|
||||||
type: 'run-finished',
|
type: 'run-finished',
|
||||||
requestId: 'main.py',
|
requestId: 'main.py',
|
||||||
|
outputs: ['/project/output.txt'],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('surfaces outputs array from run-code-result with multiple files', function () {
|
||||||
|
const { client, worker, lifecycleEvents } =
|
||||||
|
setupClientWithLifecycleTracking()
|
||||||
|
|
||||||
|
client.runCode('write_files()', { requestId: 'main.py', files: [] })
|
||||||
|
worker.emitMessage({
|
||||||
|
type: 'run-code-result',
|
||||||
|
id: 'main.py',
|
||||||
|
outputs: ['/project/fig1.png', '/project/results/data.csv'],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(lifecycleEvents).to.deep.include({
|
||||||
|
type: 'run-finished',
|
||||||
|
requestId: 'main.py',
|
||||||
|
outputs: ['/project/fig1.png', '/project/results/data.csv'],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('surfaces empty outputs when no project files were written', function () {
|
||||||
|
const { client, worker, lifecycleEvents } =
|
||||||
|
setupClientWithLifecycleTracking()
|
||||||
|
|
||||||
|
client.runCode('print("no writes")', { requestId: 'main.py', files: [] })
|
||||||
|
worker.emitMessage({
|
||||||
|
type: 'run-code-result',
|
||||||
|
id: 'main.py',
|
||||||
|
outputs: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(lifecycleEvents).to.deep.include({
|
||||||
|
type: 'run-finished',
|
||||||
|
requestId: 'main.py',
|
||||||
|
outputs: [],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user