Skip to content

Commit

Permalink
Lite: Set the home dir path per appId at each runtime (#6432)
Browse files Browse the repository at this point in the history
* Set the home dir path per appId at each runtime

* Add a comment

* add changeset

---------

Co-authored-by: gradio-pr-bot <gradio-pr-bot@users.noreply.github.com>
Co-authored-by: Ali Abdalla <ali.si3luwa@gmail.com>
  • Loading branch information
3 people committed Dec 11, 2023
1 parent 9cff3cd commit bdf81fe
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 19 deletions.
6 changes: 6 additions & 0 deletions .changeset/true-terms-happen.md
@@ -0,0 +1,6 @@
---
"@gradio/wasm": minor
"gradio": minor
---

feat:Lite: Set the home dir path per appId at each runtime
17 changes: 10 additions & 7 deletions js/wasm/src/webworker/file.ts
@@ -1,13 +1,16 @@
import path from "path-browserify";
import type { PyodideInterface } from "pyodide";

export function addAppIdIfRelative(appId: string, filePath: string): string {
if (path.isAbsolute(filePath)) {
return filePath;
}

return path.join(appId, filePath);
}
export const globalHomeDir = "/home/pyodide";
export const getAppHomeDir = (appId: string): string =>
`${globalHomeDir}/${appId}`;
export const resolveAppHomeBasedPath = (
appId: string,
filePath: string
): string => {
const normalized = path.normalize(filePath);
return path.resolve(getAppHomeDir(appId), filePath);
};

function ensureParent(pyodide: PyodideInterface, filePath: string): void {
const normalized = path.normalize(filePath);
Expand Down
37 changes: 27 additions & 10 deletions js/wasm/src/webworker/index.ts
Expand Up @@ -14,7 +14,8 @@ import type {
import {
writeFileWithParents,
renameWithParents,
addAppIdIfRelative
getAppHomeDir,
resolveAppHomeBasedPath
} from "./file";
import { verifyRequirements } from "./requirements";
import { makeHttpRequest } from "./http";
Expand All @@ -36,8 +37,17 @@ let call_asgi_app_from_js: (
receive: () => Promise<unknown>,
send: (event: any) => Promise<void>
) => Promise<void>;
let run_code: (appId: string, code: string, path?: string) => Promise<void>;
let run_script: (appId: string, path: string) => Promise<void>;
let run_code: (
appId: string,
home_dir: string,
code: string,
path?: string
) => Promise<void>;
let run_script: (
appId: string,
home_dir: string,
path: string
) => Promise<void>;
let unload_local_modules: (target_dir_path?: string) => void;

async function initializeEnvironment(
Expand Down Expand Up @@ -192,7 +202,7 @@ async function initializeApp(
}
const { opts } = options.files[path];

const appifiedPath = addAppIdIfRelative(appId, path);
const appifiedPath = resolveAppHomeBasedPath(appId, path);
console.debug(`Write a file "${appifiedPath}"`);
writeFileWithParents(pyodide, appifiedPath, data, opts);
})
Expand Down Expand Up @@ -228,6 +238,13 @@ if ("postMessage" in ctx) {
let envReadyPromise: Promise<void> | undefined = undefined;

function setupMessageHandler(receiver: MessageTransceiver): void {
// A concept of "app" is introduced to support multiple apps in a single worker.
// Each app has its own home directory (`getAppHomeDir(appId)`) in a shared single Pyodide filesystem.
// The home directory is used as the current working directory for the app.
// Each frontend app has a connection to the worker which is the `receiver` object passed above
// and it is associated with one app.
// One app also has one Gradio server app which is managed by the `gradio.wasm_utils` module.`
// This multi-app mechanism was introduced for a SharedWorker, but the same mechanism is used for a DedicatedWorker as well.
const appId = generateRandomString(8);

const updateProgress = (log: string): void => {
Expand Down Expand Up @@ -312,7 +329,7 @@ function setupMessageHandler(receiver: MessageTransceiver): void {
case "run-python-code": {
unload_local_modules();

await run_code(appId, msg.data.code);
await run_code(appId, getAppHomeDir(appId), msg.data.code);

const replyMessage: ReplyMessageSuccess = {
type: "reply:success",
Expand All @@ -324,7 +341,7 @@ function setupMessageHandler(receiver: MessageTransceiver): void {
case "run-python-file": {
unload_local_modules();

await run_script(appId, addAppIdIfRelative(appId, msg.data.path));
await run_script(appId, getAppHomeDir(appId), msg.data.path);

const replyMessage: ReplyMessageSuccess = {
type: "reply:success",
Expand Down Expand Up @@ -362,7 +379,7 @@ function setupMessageHandler(receiver: MessageTransceiver): void {
case "file:write": {
const { path, data: fileData, opts } = msg.data;

const appifiedPath = addAppIdIfRelative(appId, path);
const appifiedPath = resolveAppHomeBasedPath(appId, path);

console.debug(`Write a file "${appifiedPath}"`);
writeFileWithParents(pyodide, appifiedPath, fileData, opts);
Expand All @@ -377,8 +394,8 @@ function setupMessageHandler(receiver: MessageTransceiver): void {
case "file:rename": {
const { oldPath, newPath } = msg.data;

const appifiedOldPath = addAppIdIfRelative(appId, oldPath);
const appifiedNewPath = addAppIdIfRelative(appId, newPath);
const appifiedOldPath = resolveAppHomeBasedPath(appId, oldPath);
const appifiedNewPath = resolveAppHomeBasedPath(appId, newPath);
console.debug(`Rename "${appifiedOldPath}" to ${appifiedNewPath}`);
renameWithParents(pyodide, appifiedOldPath, appifiedNewPath);

Expand All @@ -392,7 +409,7 @@ function setupMessageHandler(receiver: MessageTransceiver): void {
case "file:unlink": {
const { path } = msg.data;

const appifiedPath = addAppIdIfRelative(appId, path);
const appifiedPath = resolveAppHomeBasedPath(appId, path);

console.debug(`Remove "${appifiedPath}`);
pyodide.FS.unlink(appifiedPath);
Expand Down
14 changes: 12 additions & 2 deletions js/wasm/src/webworker/py/script_runner.py
@@ -1,4 +1,5 @@
import ast
import os
import sys
import tokenize
import types
Expand Down Expand Up @@ -85,22 +86,31 @@ def _new_module(name: str) -> types.ModuleType:
return types.ModuleType(name)


async def _run_script(app_id: str, script_path: str) -> None:
def set_home_dir(home_dir: str) -> None:
os.environ["HOME"] = home_dir
os.chdir(home_dir)


async def _run_script(app_id: str, home_dir: str, script_path: str) -> None:
# This function is based on the following code from Streamlit:
# https://github.com/streamlit/streamlit/blob/1.24.0/lib/streamlit/runtime/scriptrunner/script_runner.py#L519-L554
# with modifications to support top-level await.
set_home_dir(home_dir)

with tokenize.open(script_path) as f:
filebody = f.read()

await _run_code(app_id, filebody, script_path)
await _run_code(app_id, home_dir, filebody, script_path)


async def _run_code(
app_id: str,
home_dir: str,
filebody: str,
script_path: str = '<string>' # This default value follows the convention. Ref: https://docs.python.org/3/library/functions.html#compile
) -> None:
set_home_dir(home_dir)

# NOTE: In Streamlit, the bytecode caching mechanism has been introduced.
# However, we skipped it here for simplicity and because Gradio doesn't need to rerun the script so frequently,
# while we may do it in the future.
Expand Down

0 comments on commit bdf81fe

Please sign in to comment.