Skip to content

Commit 45df1b1

Browse files
Lite: Capture stdout and stderr from the main thread (#9984)
* Add stdout and stderr events * add changeset * Refactoring * Format App.tsx * add changeset * Add python-error event to capture Python errors occurring in the running event loop after the initial app launch * Fix <ErrorDisplay />'s close button * Fix <ErrorDisplay /> * Propagate python-error and initialization-error events to the controller * Add init-code|file-run-error events --------- Co-authored-by: gradio-pr-bot <[email protected]>
1 parent 9285dd9 commit 45df1b1

13 files changed

+308
-56
lines changed

.changeset/pink-signs-fall.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@gradio/lite": minor
3+
"@gradio/wasm": minor
4+
"gradio": minor
5+
---
6+
7+
feat:Lite: Capture stdout and stderr from the main thread

gradio/queueing.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
import fastapi
1515

16-
from gradio import route_utils, routes
16+
from gradio import route_utils, routes, wasm_utils
1717
from gradio.data_classes import (
1818
PredictBodyInternal,
1919
)
@@ -644,6 +644,7 @@ async def process_events(
644644
err = e
645645
for event in awake_events:
646646
content = error_payload(err, app.get_blocks().show_error)
647+
wasm_utils.send_error(err)
647648
self.send_message(
648649
event,
649650
ProcessCompletedMessage(
@@ -736,6 +737,7 @@ async def process_events(
736737
success = False
737738
error = err or old_err
738739
output = error_payload(error, app.get_blocks().show_error)
740+
wasm_utils.send_error(error)
739741
for event in awake_events:
740742
self.send_message(
741743
event, ProcessCompletedMessage(output=output, success=success)

gradio/wasm_utils.py

+32
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
from __future__ import annotations
22

3+
import logging
34
import sys
5+
import traceback
46
from contextlib import contextmanager
57
from contextvars import ContextVar
68

9+
LOGGER = logging.getLogger(__name__)
10+
711
# See https://pyodide.org/en/stable/usage/faq.html#how-to-detect-that-code-is-run-with-pyodide
812
IS_WASM = sys.platform == "emscripten"
913

@@ -57,3 +61,31 @@ def get_registered_app(app_id: str):
5761
raise GradioAppNotFoundError(
5862
f"Gradio app not found (ID: {app_id}). Forgot to call demo.launch()?"
5963
) from e
64+
65+
66+
error_traceback_callback_map = {}
67+
68+
69+
def register_error_traceback_callback(app_id, callback):
70+
error_traceback_callback_map[app_id] = callback
71+
72+
73+
def send_error(error: Exception | None):
74+
# The callback registered by the JS process is called with the error traceback
75+
# for the WebWorker process to read the traceback.
76+
77+
if not IS_WASM:
78+
return
79+
if error is None:
80+
return
81+
82+
app_id = _app_id_context_var.get()
83+
callback = error_traceback_callback_map.get(app_id)
84+
if not callback:
85+
LOGGER.warning(
86+
f"Error callback not found for the app ID {app_id}. The error will be ignored."
87+
)
88+
return
89+
90+
tb = "".join(traceback.format_exception(type(error), error, error.__traceback__))
91+
callback(tb)

js/lite/lite.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,6 @@
3030
<div style="flex-grow: 1; overflow: scroll; position: relative">
3131
<div id="gradio-app" style="min-height: 100%"></div>
3232
</div>
33-
<div id="dev-app" style="height: 300px; position: relative"></div>
33+
<div id="dev-app" style="height: 50%; position: relative"></div>
3434
</body>
3535
</html>

js/lite/src/ErrorDisplay.svelte

+45-21
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,69 @@
11
<script lang="ts">
22
import { StatusTracker } from "@gradio/statustracker";
3+
import { Embed } from "@gradio/core";
34
5+
import { createEventDispatcher } from "svelte";
46
import { _ } from "svelte-i18n";
57
import { setupi18n } from "@gradio/core";
68
79
setupi18n();
810
11+
const dispatch = createEventDispatcher();
12+
913
export let is_embed: boolean;
1014
export let error: Error | undefined = undefined;
15+
export let height: string;
16+
17+
// For <Embed>
18+
export let container: boolean;
19+
export let version: string;
20+
let wrapper: HTMLDivElement;
1121
</script>
1222

13-
<StatusTracker
14-
i18n={$_}
15-
absolute={!is_embed}
16-
status="error"
17-
timer={false}
18-
queue_position={null}
19-
queue_size={null}
20-
translucent={true}
21-
autoscroll={false}
23+
<Embed
24+
display={container && is_embed}
25+
{is_embed}
26+
info={false}
27+
{version}
28+
initial_height={height}
29+
loaded={false}
30+
space={null}
31+
fill_width={false}
32+
bind:wrapper
2233
>
23-
<div class="error" slot="error">
24-
{#if error}
25-
{#if error.message}
26-
<p class="error-name">
27-
{error.message}
28-
</p>
34+
<StatusTracker
35+
i18n={$_}
36+
absolute={!is_embed}
37+
status="error"
38+
timer={false}
39+
queue_position={null}
40+
queue_size={null}
41+
translucent={true}
42+
autoscroll={false}
43+
on:clear_status={() => dispatch("clear_error")}
44+
>
45+
<div class="error" slot="error">
46+
{#if error}
47+
{#if error.message}
48+
<p class="error-name">
49+
{error.message}
50+
</p>
51+
{/if}
52+
{#if error.stack}
53+
<pre class="error-stack"><code>{error.stack}</code></pre>
54+
{/if}
2955
{/if}
30-
{#if error.stack}
31-
<pre class="error-stack"><code>{error.stack}</code></pre>
32-
{/if}
33-
{/if}
34-
</div>
35-
</StatusTracker>
56+
</div>
57+
</StatusTracker>
58+
</Embed>
3659

3760
<style>
3861
.error {
3962
position: relative;
4063
width: 100%;
4164
padding: var(--size-4);
4265
color: var(--body-text-color);
66+
overflow: scroll;
4367
/* Status tracker sets `pointer-events: none`.
4468
Override it here so the user can scroll the element with `overflow: hidden`
4569
and copy and paste the error message */

js/lite/src/LiteIndex.svelte

+46-14
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,26 @@
5050
requirements: requirements ?? [],
5151
sharedWorkerMode: sharedWorkerMode ?? false
5252
});
53+
54+
const dispatch = createEventDispatcher();
55+
56+
worker_proxy.addEventListener("modules-auto-loaded", (event) => {
57+
dispatch("modules-auto-loaded", (event as CustomEvent).detail);
58+
});
59+
worker_proxy.addEventListener("stdout", (event) => {
60+
dispatch("stdout", (event as CustomEvent).detail);
61+
});
62+
worker_proxy.addEventListener("stderr", (event) => {
63+
dispatch("stderr", (event as CustomEvent).detail);
64+
});
65+
worker_proxy.addEventListener("initialization-error", (event) => {
66+
error = (event as CustomEvent).detail;
67+
dispatch("initialization-error", (event as CustomEvent).detail);
68+
});
69+
worker_proxy.addEventListener("python-error", (event) => {
70+
error = (event as CustomEvent).detail;
71+
dispatch("python-error", (event as CustomEvent).detail);
72+
});
5373
onDestroy(() => {
5474
worker_proxy.terminate();
5575
});
@@ -90,24 +110,18 @@
90110
worker_proxy.install.bind(worker_proxy)
91111
);
92112
93-
worker_proxy.addEventListener("initialization-error", (event) => {
94-
error = (event as CustomEvent).detail;
95-
});
96-
97-
const dispatch = createEventDispatcher();
98-
99-
worker_proxy.addEventListener("modules-auto-loaded", (event) => {
100-
dispatch("modules-auto-loaded", (event as CustomEvent).detail);
101-
});
102-
103113
// Internally, the execution of `runPythonCode()` or `runPythonFile()` is queued
104114
// and its promise will be resolved after the Pyodide is loaded and the worker initialization is done
105115
// (see the await in the `onmessage` callback in the webworker code)
106116
// So we don't await this promise because we want to mount the `Index` immediately and start the app initialization asynchronously.
107117
if (code != null) {
108-
worker_proxy.runPythonCode(code);
118+
worker_proxy.runPythonCode(code).catch((err) => {
119+
dispatch("init-code-run-error", err);
120+
});
109121
} else if (entrypoint != null) {
110-
worker_proxy.runPythonFile(entrypoint);
122+
worker_proxy.runPythonFile(entrypoint).catch((err) => {
123+
dispatch("init-file-run-error", err);
124+
});
111125
} else {
112126
throw new Error("Either code or entrypoint must be provided.");
113127
}
@@ -157,7 +171,16 @@
157171
>
158172
{#key index_component_key}
159173
{#if error}
160-
<ErrorDisplay {error} is_embed />
174+
<ErrorDisplay
175+
{error}
176+
{is_embed}
177+
height={initial_height}
178+
{container}
179+
{version}
180+
on:clear_error={() => {
181+
error = null;
182+
}}
183+
/>
161184
{:else}
162185
<Index
163186
space={null}
@@ -182,7 +205,16 @@
182205
{:else}
183206
{#key index_component_key}
184207
{#if error}
185-
<ErrorDisplay {error} {is_embed} />
208+
<ErrorDisplay
209+
{error}
210+
{is_embed}
211+
height={initial_height}
212+
{container}
213+
{version}
214+
on:clear_error={() => {
215+
error = null;
216+
}}
217+
/>
186218
{:else}
187219
<Index
188220
space={null}

js/lite/src/dev/App.svelte

+51
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,9 @@ def hi(name):
5959
6060
const requirements = parse_requirements(requirements_txt);
6161
62+
let stdouts: string[] = [];
63+
let stderrs: string[] = [];
64+
6265
let controller: ReturnType<typeof create>;
6366
onMount(() => {
6467
controller = create({
@@ -85,6 +88,14 @@ def hi(name):
8588
requirements_txt +=
8689
"\n" + packageNames.map((line) => line + " # auto-loaded").join("\n");
8790
});
91+
controller.addEventListener("stdout", (event) => {
92+
const message = (event as CustomEvent).detail as string;
93+
stdouts = stdouts.concat(message);
94+
});
95+
controller.addEventListener("stderr", (event) => {
96+
const message = (event as CustomEvent).detail as string;
97+
stderrs = stderrs.concat(message);
98+
});
8899
});
89100
onDestroy(() => {
90101
controller.unmount();
@@ -124,6 +135,27 @@ def hi(name):
124135
</script>
125136

126137
<div class="container">
138+
<div class="panel">
139+
<div class="log-panel-container">
140+
<div class="log-panel">
141+
<h4>stdout</h4>
142+
<div class="log-box" id="stdout" style="color: black;">
143+
{#each stdouts as stdout}
144+
<pre class="log-line">{stdout}</pre>
145+
{/each}
146+
</div>
147+
</div>
148+
<div class="log-panel">
149+
<h4>stdout</h4>
150+
<div class="log-box" id="stderr" style="color: red;">
151+
{#each stderrs as stderr}
152+
<pre class="log-line">{stderr}</pre>
153+
{/each}
154+
</div>
155+
</div>
156+
</div>
157+
</div>
158+
127159
<div class="panel">
128160
When the SharedWorker mode is enabled, access the URL below (for Chrome) and
129161
click the "inspect" link of the worker to show the console log emitted from
@@ -184,6 +216,25 @@ def hi(name):
184216
width: 100%;
185217
}
186218
219+
.log-panel-container {
220+
height: 300px;
221+
position: relative;
222+
display: flex;
223+
flex-direction: row;
224+
}
225+
.log-panel {
226+
width: 50%;
227+
display: flex;
228+
flex-direction: column;
229+
}
230+
.log-box {
231+
flex-grow: 1;
232+
overflow: scroll;
233+
}
234+
.log-line {
235+
margin: 0;
236+
}
237+
187238
.cell-header {
188239
display: flex;
189240
flex-direction: row;

js/lite/src/index.ts

+26
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,32 @@ export class GradioAppController extends EventTarget {
3333
new CustomEvent("modules-auto-loaded", { detail: event.detail })
3434
);
3535
});
36+
this.lite_svelte_app.$on("stdout", (event: CustomEvent) => {
37+
this.dispatchEvent(new CustomEvent("stdout", { detail: event.detail }));
38+
});
39+
this.lite_svelte_app.$on("stderr", (event: CustomEvent) => {
40+
this.dispatchEvent(new CustomEvent("stderr", { detail: event.detail }));
41+
});
42+
this.lite_svelte_app.$on("initialization-error", (event: CustomEvent) => {
43+
this.dispatchEvent(
44+
new CustomEvent("initialization-error", { detail: event.detail })
45+
);
46+
});
47+
this.lite_svelte_app.$on("python-error", (event: CustomEvent) => {
48+
this.dispatchEvent(
49+
new CustomEvent("python-error", { detail: event.detail })
50+
);
51+
});
52+
this.lite_svelte_app.$on("init-code-run-error", (event: CustomEvent) => {
53+
this.dispatchEvent(
54+
new CustomEvent("init-code-run-error", { detail: event.detail })
55+
);
56+
});
57+
this.lite_svelte_app.$on("init-file-run-error", (event: CustomEvent) => {
58+
this.dispatchEvent(
59+
new CustomEvent("init-file-run-error", { detail: event.detail })
60+
);
61+
});
3662
}
3763

3864
run_code = (code: string): Promise<void> => {

0 commit comments

Comments
 (0)