Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 21 additions & 2 deletions packages/dev/inspector-v2/src/cli/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ export async function StartBridge(config: IBridgeConfig): Promise<IBridgeHandle>
break;
}
case "commandListResponse":
case "commandResponse": {
case "commandResponse":
case "infoResponse": {
// Forward response back to the CLI that requested it.
const resolve = pendingBrowserRequests.get(message.requestId);
if (resolve) {
Expand Down Expand Up @@ -165,9 +166,27 @@ export async function StartBridge(config: IBridgeConfig): Promise<IBridgeHandle>
case "sessions": {
// Wait for at least one session to connect before responding.
await waitForSession();

// Query each session for its current name.
const updatedNames = new Map<number, string>();
const infoPromises = Array.from(sessions.values()).map(async (s) => {
const requestId = generateRequestId();
sendBrowserRequest(s, { type: "getInfo", requestId });
try {
const raw = await waitForBrowserResponse(requestId, 5000);
const info = JSON.parse(raw);
if (info.type === "infoResponse" && typeof info.name === "string") {
updatedNames.set(s.id, info.name);
}
} catch {
// Keep the existing name if the session doesn't respond.
}
});
await Promise.all(infoPromises);

const sessionList: SessionInfo[] = Array.from(sessions.values()).map((s) => ({
id: s.id,
name: s.name,
name: updatedNames.get(s.id) ?? s.name,
connectedAt: s.connectedAt,
}));
sendCliResponse({ type: "sessionsResponse", sessions: sessionList });
Expand Down
18 changes: 17 additions & 1 deletion packages/dev/inspector-v2/src/cli/cli.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* eslint-disable no-console */
import { spawn } from "child_process";
import { dirname, join, resolve } from "path";
import { readFileSync } from "fs";
import { fileURLToPath } from "url";
import { parseArgs } from "util";
import { WebSocket } from "./webSocket.js";
Expand Down Expand Up @@ -320,7 +321,8 @@ export function PrintCommandHelp(commandId: string, descriptor: CommandInfo, mis
const maxLen = Math.max(...descriptor.args.map((a) => `--${a.name}${a.required ? " (required)" : ""}`.length));
for (const arg of descriptor.args) {
const label = `--${arg.name}${arg.required ? " (required)" : ""}`;
console.log(` ${label.padEnd(maxLen)} ${arg.description}`);
const typeHint = arg.type === "file" ? " [reads file content from path]" : "";
console.log(` ${label.padEnd(maxLen)} ${arg.description}${typeHint}`);
}
}
if (missingRequired.length > 0 && !wantsHelp) {
Expand Down Expand Up @@ -404,6 +406,20 @@ async function HandleCommand(socket: WebSocket, args: IParsedArgs): Promise<void
return;
}

// Resolve "file" type arguments: read the file and replace the value with its contents.
for (const argDef of descriptor.args ?? []) {
if (argDef.type === "file" && argDef.name in commandArgs) {
const filePath = resolve(commandArgs[argDef.name]);
try {
commandArgs[argDef.name] = readFileSync(filePath, "utf-8");
} catch (err: unknown) {
console.error(`Error reading file for --${argDef.name}: ${err}`);
process.exitCode = 1;
return;
}
}
}

const response = await SendAndReceive<ExecResponse>(socket, {
type: "exec",
sessionId,
Expand Down
28 changes: 26 additions & 2 deletions packages/dev/inspector-v2/src/cli/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export type CommandArgInfo = {
description: string;
/** Whether this argument is required. */
required?: boolean;
/** The type of the argument. Defaults to "string". When "file", the CLI reads the file and sends its contents. */
type?: "string" | "file";
};

/**
Expand Down Expand Up @@ -170,10 +172,22 @@ export type CommandResponse = {
error?: string;
};

/**
* Browser → Bridge: Response to a getInfo request from the bridge.
*/
export type InfoResponse = {
/** The message type discriminator. */
type: "infoResponse";
/** The identifier of the original request. */
requestId: string;
/** The current display name of the session. */
name: string;
};

/**
* All messages that the browser sends to the bridge.
*/
export type BrowserRequest = RegisterRequest | CommandListResponse | CommandResponse;
export type BrowserRequest = RegisterRequest | CommandListResponse | CommandResponse | InfoResponse;

/**
* Bridge → Browser: Request the list of registered commands.
Expand All @@ -199,7 +213,17 @@ export type ExecCommandRequest = {
args: Record<string, string>;
};

/**
* Bridge → Browser: Request current session information.
*/
export type GetInfoRequest = {
/** The message type discriminator. */
type: "getInfo";
/** A unique identifier for this request. */
requestId: string;
};

/**
* All messages that the bridge sends to the browser.
*/
export type BrowserResponse = ListCommandsRequest | ExecCommandRequest;
export type BrowserResponse = ListCommandsRequest | ExecCommandRequest | GetInfoRequest;
8 changes: 7 additions & 1 deletion packages/dev/inspector-v2/src/inspectable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export type InternalInspectableToken = InspectableToken & {
// Track shared state per scene: the service container, ref count, and teardown logic.
type InspectableState = {
refCount: number;
readonly fullyDisposed: boolean;
serviceContainer: ServiceContainer;
sceneDisposeObserver: { remove: () => void };
fullyDispose: () => void;
Expand All @@ -91,8 +92,10 @@ export function _StartInspectable(scene: Scene, options?: Partial<InspectableOpt

const serviceContainer = new ServiceContainer("InspectableContainer");

let fullyDisposed = false;
const fullyDispose = () => {
InspectableStates.delete(scene);
fullyDisposed = true;
serviceContainer.dispose();
sceneDisposeObserver.remove();
};
Expand Down Expand Up @@ -125,6 +128,9 @@ export function _StartInspectable(scene: Scene, options?: Partial<InspectableOpt

state = {
refCount: 0,
get fullyDisposed() {
return fullyDisposed;
},
serviceContainer,
sceneDisposeObserver: { remove: () => {} },
fullyDispose,
Expand Down Expand Up @@ -182,7 +188,7 @@ export function _StartInspectable(scene: Scene, options?: Partial<InspectableOpt
let disposed = false;
const token: InternalInspectableToken = {
get isDisposed() {
return disposed;
return disposed || owningState.fullyDisposed;
},
get serviceContainer() {
return serviceContainer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export interface IInspectableBridgeServiceOptions {

/**
* The session display name sent to the bridge.
* Can be a getter to provide a dynamic value that is re-read
* each time the bridge queries session information.
*/
name: string;

Expand Down Expand Up @@ -140,6 +142,14 @@ export function MakeInspectableBridgeServiceDefinition(options: IInspectableBrid
});
break;
}
case "getInfo": {
sendToBridge({
type: "infoResponse",
requestId: message.requestId,
name: options.name,
});
break;
}
case "execCommand": {
const command = commands.get(message.commandId);
if (!command) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { type IDisposable } from "core/index";
import { type IService } from "shared-ui-components/modularTool/modularity/serviceDefinition";

/**
* The type of an inspectable command argument, which determines how
* the CLI processes the value before sending it to the browser.
*/
export type InspectableCommandArgType = "string" | "file";

/**
* Describes an argument for an inspectable command.
*/
Expand All @@ -19,6 +25,13 @@ export type InspectableCommandArg = {
* Whether the argument is required.
*/
required?: boolean;

/**
* The type of the argument. Defaults to "string".
* When set to "file", the CLI reads the file at the given path
* and passes its contents as the argument value.
*/
type?: InspectableCommandArgType;
};

/**
Expand Down
53 changes: 53 additions & 0 deletions packages/dev/inspector-v2/test/unit/cli/bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type CommandResponse,
type CommandsResponse,
type ExecResponse,
type InfoResponse,
type SessionsResponse,
type StopResponse,
} from "../../../src/cli/protocol";
Expand Down Expand Up @@ -55,6 +56,24 @@ function tick(ms = 50): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}

function autoRespondGetInfo(ws: WebSocket, name: string | (() => string)): void {
ws.on("message", (data) => {
try {
const msg = JSON.parse(data.toString());
if (msg.type === "getInfo") {
const response: InfoResponse = {
type: "infoResponse",
requestId: msg.requestId,
name: typeof name === "function" ? name() : name,
};
ws.send(JSON.stringify(response));
}
} catch {
// ignore
}
});
}

describe("Inspector Bridge", () => {
let bridge: IBridgeHandle;

Expand All @@ -76,6 +95,7 @@ describe("Inspector Bridge", () => {

it("registers a browser session and lists it via CLI", async () => {
const browser = await connect(bridge.browserPort);
autoRespondGetInfo(browser, "Test Scene");
browser.send(JSON.stringify({ type: "register", name: "Test Scene" }));
await tick();

Expand Down Expand Up @@ -110,6 +130,7 @@ describe("Inspector Bridge", () => {

it("removes session when browser disconnects", async () => {
const browser = await connect(bridge.browserPort);
autoRespondGetInfo(browser, "Temporary Scene");
browser.send(JSON.stringify({ type: "register", name: "Temporary Scene" }));
await tick();

Expand All @@ -132,6 +153,7 @@ describe("Inspector Bridge", () => {

it("forwards command listing from browser to CLI", async () => {
const browser = await connect(bridge.browserPort);
autoRespondGetInfo(browser, "Scene");
browser.send(JSON.stringify({ type: "register", name: "Scene" }));
await tick();

Expand Down Expand Up @@ -166,6 +188,7 @@ describe("Inspector Bridge", () => {

it("forwards command execution from CLI to browser", async () => {
const browser = await connect(bridge.browserPort);
autoRespondGetInfo(browser, "Scene");
browser.send(JSON.stringify({ type: "register", name: "Scene" }));
await tick();

Expand Down Expand Up @@ -237,10 +260,12 @@ describe("Inspector Bridge", () => {

it("supports multiple browser sessions", async () => {
const browser1 = await connect(bridge.browserPort);
autoRespondGetInfo(browser1, "Scene A");
browser1.send(JSON.stringify({ type: "register", name: "Scene A" }));
await tick();

const browser2 = await connect(bridge.browserPort);
autoRespondGetInfo(browser2, "Scene B");
browser2.send(JSON.stringify({ type: "register", name: "Scene B" }));
await tick();

Expand All @@ -258,6 +283,7 @@ describe("Inspector Bridge", () => {

it("ignores malformed JSON on browser port", async () => {
const browser = await connect(bridge.browserPort);
autoRespondGetInfo(browser, "After Bad JSON");
browser.send("not valid json{{{");
await tick();

Expand All @@ -281,6 +307,7 @@ describe("Inspector Bridge", () => {

// Bridge should still be functional — send a valid request.
const browser = await connect(bridge.browserPort);
autoRespondGetInfo(browser, "Scene");
browser.send(JSON.stringify({ type: "register", name: "Scene" }));
await tick();

Expand All @@ -290,4 +317,30 @@ describe("Inspector Bridge", () => {
await close(browser);
await close(cli);
});

it("returns updated session name via getInfo on each listing", async () => {
let currentName = "Initial Name";
const browser = await connect(bridge.browserPort);
autoRespondGetInfo(browser, () => currentName);
browser.send(JSON.stringify({ type: "register", name: "Initial Name" }));
await tick();

const cli = await connect(bridge.cliPort);

// First listing returns the initial name.
const first = await sendAndReceive<SessionsResponse>(cli, { type: "sessions" });
expect(first.sessions).toHaveLength(1);
expect(first.sessions[0].name).toBe("Initial Name");

// Update the name on the browser side.
currentName = "Updated Name";

// Second listing reflects the updated name.
const second = await sendAndReceive<SessionsResponse>(cli, { type: "sessions" });
expect(second.sessions).toHaveLength(1);
expect(second.sessions[0].name).toBe("Updated Name");

await close(browser);
await close(cli);
});
});
2 changes: 1 addition & 1 deletion packages/tools/playground/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
dataLayer.push(arguments);
}
gtag("js", new Date());
window.__PLAYGROUND_BUNDLE__ = '<%= PLAYGROUND_BUNDLE %>';
window.__PLAYGROUND_BUNDLE__ = "<%= PLAYGROUND_BUNDLE %>";
gtag("config", "UA-41767310-2");
</script>
<title>Babylon.js Playground</title>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export class MonacoComponent extends React.Component<IMonacoComponentProps, ICom
if (gs.activeEditorPath !== p) {
gs.activeEditorPath = p;
gs.onActiveEditorChangedObservable?.notifyObservers();
this._monacoManager.switchActiveFile(p);
}
this.setState((s) => ({ ...s }));
})
Expand Down
Loading