Skip to content

Commit

Permalink
Lite: Capture stdout and stderr from the main thread (#9984)
Browse files Browse the repository at this point in the history
* 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 <gradio-pr-bot@users.noreply.github.com>
  • Loading branch information
whitphx and gradio-pr-bot authored Dec 23, 2024
1 parent 9285dd9 commit 45df1b1
Show file tree
Hide file tree
Showing 13 changed files with 308 additions and 56 deletions.
7 changes: 7 additions & 0 deletions .changeset/pink-signs-fall.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@gradio/lite": minor
"@gradio/wasm": minor
"gradio": minor
---

feat:Lite: Capture stdout and stderr from the main thread
4 changes: 3 additions & 1 deletion gradio/queueing.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

import fastapi

from gradio import route_utils, routes
from gradio import route_utils, routes, wasm_utils
from gradio.data_classes import (
PredictBodyInternal,
)
Expand Down Expand Up @@ -644,6 +644,7 @@ async def process_events(
err = e
for event in awake_events:
content = error_payload(err, app.get_blocks().show_error)
wasm_utils.send_error(err)
self.send_message(
event,
ProcessCompletedMessage(
Expand Down Expand Up @@ -736,6 +737,7 @@ async def process_events(
success = False
error = err or old_err
output = error_payload(error, app.get_blocks().show_error)
wasm_utils.send_error(error)
for event in awake_events:
self.send_message(
event, ProcessCompletedMessage(output=output, success=success)
Expand Down
32 changes: 32 additions & 0 deletions gradio/wasm_utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
from __future__ import annotations

import logging
import sys
import traceback
from contextlib import contextmanager
from contextvars import ContextVar

LOGGER = logging.getLogger(__name__)

# See https://pyodide.org/en/stable/usage/faq.html#how-to-detect-that-code-is-run-with-pyodide
IS_WASM = sys.platform == "emscripten"

Expand Down Expand Up @@ -57,3 +61,31 @@ def get_registered_app(app_id: str):
raise GradioAppNotFoundError(
f"Gradio app not found (ID: {app_id}). Forgot to call demo.launch()?"
) from e


error_traceback_callback_map = {}


def register_error_traceback_callback(app_id, callback):
error_traceback_callback_map[app_id] = callback


def send_error(error: Exception | None):
# The callback registered by the JS process is called with the error traceback
# for the WebWorker process to read the traceback.

if not IS_WASM:
return
if error is None:
return

app_id = _app_id_context_var.get()
callback = error_traceback_callback_map.get(app_id)
if not callback:
LOGGER.warning(
f"Error callback not found for the app ID {app_id}. The error will be ignored."
)
return

tb = "".join(traceback.format_exception(type(error), error, error.__traceback__))
callback(tb)
2 changes: 1 addition & 1 deletion js/lite/lite.html
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@
<div style="flex-grow: 1; overflow: scroll; position: relative">
<div id="gradio-app" style="min-height: 100%"></div>
</div>
<div id="dev-app" style="height: 300px; position: relative"></div>
<div id="dev-app" style="height: 50%; position: relative"></div>
</body>
</html>
66 changes: 45 additions & 21 deletions js/lite/src/ErrorDisplay.svelte
Original file line number Diff line number Diff line change
@@ -1,45 +1,69 @@
<script lang="ts">
import { StatusTracker } from "@gradio/statustracker";
import { Embed } from "@gradio/core";
import { createEventDispatcher } from "svelte";
import { _ } from "svelte-i18n";
import { setupi18n } from "@gradio/core";
setupi18n();
const dispatch = createEventDispatcher();
export let is_embed: boolean;
export let error: Error | undefined = undefined;
export let height: string;
// For <Embed>
export let container: boolean;
export let version: string;
let wrapper: HTMLDivElement;
</script>

<StatusTracker
i18n={$_}
absolute={!is_embed}
status="error"
timer={false}
queue_position={null}
queue_size={null}
translucent={true}
autoscroll={false}
<Embed
display={container && is_embed}
{is_embed}
info={false}
{version}
initial_height={height}
loaded={false}
space={null}
fill_width={false}
bind:wrapper
>
<div class="error" slot="error">
{#if error}
{#if error.message}
<p class="error-name">
{error.message}
</p>
<StatusTracker
i18n={$_}
absolute={!is_embed}
status="error"
timer={false}
queue_position={null}
queue_size={null}
translucent={true}
autoscroll={false}
on:clear_status={() => dispatch("clear_error")}
>
<div class="error" slot="error">
{#if error}
{#if error.message}
<p class="error-name">
{error.message}
</p>
{/if}
{#if error.stack}
<pre class="error-stack"><code>{error.stack}</code></pre>
{/if}
{/if}
{#if error.stack}
<pre class="error-stack"><code>{error.stack}</code></pre>
{/if}
{/if}
</div>
</StatusTracker>
</div>
</StatusTracker>
</Embed>

<style>
.error {
position: relative;
width: 100%;
padding: var(--size-4);
color: var(--body-text-color);
overflow: scroll;
/* Status tracker sets `pointer-events: none`.
Override it here so the user can scroll the element with `overflow: hidden`
and copy and paste the error message */
Expand Down
60 changes: 46 additions & 14 deletions js/lite/src/LiteIndex.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,26 @@
requirements: requirements ?? [],
sharedWorkerMode: sharedWorkerMode ?? false
});
const dispatch = createEventDispatcher();
worker_proxy.addEventListener("modules-auto-loaded", (event) => {
dispatch("modules-auto-loaded", (event as CustomEvent).detail);
});
worker_proxy.addEventListener("stdout", (event) => {
dispatch("stdout", (event as CustomEvent).detail);
});
worker_proxy.addEventListener("stderr", (event) => {
dispatch("stderr", (event as CustomEvent).detail);
});
worker_proxy.addEventListener("initialization-error", (event) => {
error = (event as CustomEvent).detail;
dispatch("initialization-error", (event as CustomEvent).detail);
});
worker_proxy.addEventListener("python-error", (event) => {
error = (event as CustomEvent).detail;
dispatch("python-error", (event as CustomEvent).detail);
});
onDestroy(() => {
worker_proxy.terminate();
});
Expand Down Expand Up @@ -90,24 +110,18 @@
worker_proxy.install.bind(worker_proxy)
);
worker_proxy.addEventListener("initialization-error", (event) => {
error = (event as CustomEvent).detail;
});
const dispatch = createEventDispatcher();
worker_proxy.addEventListener("modules-auto-loaded", (event) => {
dispatch("modules-auto-loaded", (event as CustomEvent).detail);
});
// Internally, the execution of `runPythonCode()` or `runPythonFile()` is queued
// and its promise will be resolved after the Pyodide is loaded and the worker initialization is done
// (see the await in the `onmessage` callback in the webworker code)
// So we don't await this promise because we want to mount the `Index` immediately and start the app initialization asynchronously.
if (code != null) {
worker_proxy.runPythonCode(code);
worker_proxy.runPythonCode(code).catch((err) => {
dispatch("init-code-run-error", err);
});
} else if (entrypoint != null) {
worker_proxy.runPythonFile(entrypoint);
worker_proxy.runPythonFile(entrypoint).catch((err) => {
dispatch("init-file-run-error", err);
});
} else {
throw new Error("Either code or entrypoint must be provided.");
}
Expand Down Expand Up @@ -157,7 +171,16 @@
>
{#key index_component_key}
{#if error}
<ErrorDisplay {error} is_embed />
<ErrorDisplay
{error}
{is_embed}
height={initial_height}
{container}
{version}
on:clear_error={() => {
error = null;
}}
/>
{:else}
<Index
space={null}
Expand All @@ -182,7 +205,16 @@
{:else}
{#key index_component_key}
{#if error}
<ErrorDisplay {error} {is_embed} />
<ErrorDisplay
{error}
{is_embed}
height={initial_height}
{container}
{version}
on:clear_error={() => {
error = null;
}}
/>
{:else}
<Index
space={null}
Expand Down
51 changes: 51 additions & 0 deletions js/lite/src/dev/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ def hi(name):
const requirements = parse_requirements(requirements_txt);
let stdouts: string[] = [];
let stderrs: string[] = [];
let controller: ReturnType<typeof create>;
onMount(() => {
controller = create({
Expand All @@ -85,6 +88,14 @@ def hi(name):
requirements_txt +=
"\n" + packageNames.map((line) => line + " # auto-loaded").join("\n");
});
controller.addEventListener("stdout", (event) => {
const message = (event as CustomEvent).detail as string;
stdouts = stdouts.concat(message);
});
controller.addEventListener("stderr", (event) => {
const message = (event as CustomEvent).detail as string;
stderrs = stderrs.concat(message);
});
});
onDestroy(() => {
controller.unmount();
Expand Down Expand Up @@ -124,6 +135,27 @@ def hi(name):
</script>

<div class="container">
<div class="panel">
<div class="log-panel-container">
<div class="log-panel">
<h4>stdout</h4>
<div class="log-box" id="stdout" style="color: black;">
{#each stdouts as stdout}
<pre class="log-line">{stdout}</pre>
{/each}
</div>
</div>
<div class="log-panel">
<h4>stdout</h4>
<div class="log-box" id="stderr" style="color: red;">
{#each stderrs as stderr}
<pre class="log-line">{stderr}</pre>
{/each}
</div>
</div>
</div>
</div>

<div class="panel">
When the SharedWorker mode is enabled, access the URL below (for Chrome) and
click the "inspect" link of the worker to show the console log emitted from
Expand Down Expand Up @@ -184,6 +216,25 @@ def hi(name):
width: 100%;
}
.log-panel-container {
height: 300px;
position: relative;
display: flex;
flex-direction: row;
}
.log-panel {
width: 50%;
display: flex;
flex-direction: column;
}
.log-box {
flex-grow: 1;
overflow: scroll;
}
.log-line {
margin: 0;
}
.cell-header {
display: flex;
flex-direction: row;
Expand Down
26 changes: 26 additions & 0 deletions js/lite/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,32 @@ export class GradioAppController extends EventTarget {
new CustomEvent("modules-auto-loaded", { detail: event.detail })
);
});
this.lite_svelte_app.$on("stdout", (event: CustomEvent) => {
this.dispatchEvent(new CustomEvent("stdout", { detail: event.detail }));
});
this.lite_svelte_app.$on("stderr", (event: CustomEvent) => {
this.dispatchEvent(new CustomEvent("stderr", { detail: event.detail }));
});
this.lite_svelte_app.$on("initialization-error", (event: CustomEvent) => {
this.dispatchEvent(
new CustomEvent("initialization-error", { detail: event.detail })
);
});
this.lite_svelte_app.$on("python-error", (event: CustomEvent) => {
this.dispatchEvent(
new CustomEvent("python-error", { detail: event.detail })
);
});
this.lite_svelte_app.$on("init-code-run-error", (event: CustomEvent) => {
this.dispatchEvent(
new CustomEvent("init-code-run-error", { detail: event.detail })
);
});
this.lite_svelte_app.$on("init-file-run-error", (event: CustomEvent) => {
this.dispatchEvent(
new CustomEvent("init-file-run-error", { detail: event.detail })
);
});
}

run_code = (code: string): Promise<void> => {
Expand Down
Loading

0 comments on commit 45df1b1

Please sign in to comment.