Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
78c309f
Initial pass
ryantrem Mar 30, 2026
64c3756
Move files into cli directory
ryantrem Mar 30, 2026
b6abefe
Protocol
ryantrem Mar 30, 2026
f0a490d
Fix unit tests
ryantrem Mar 30, 2026
87f6f68
Add query mesh command
ryantrem Mar 30, 2026
838d936
SessionId is optional when there is only one session
ryantrem Mar 30, 2026
7f54d5e
Execute with --command and include per command help
ryantrem Mar 30, 2026
c3f7395
Add more entity commands
ryantrem Mar 30, 2026
9ef8a1e
Fix lint
ryantrem Mar 30, 2026
159df9d
Add support for listing entities with basic info
ryantrem Mar 30, 2026
7c86fb1
Add screenshot command
ryantrem Mar 30, 2026
6e6c4c4
Aligned descriptions
ryantrem Mar 31, 2026
3d4bbab
Add missing entity types
ryantrem Mar 31, 2026
0052116
Merge master
ryantrem Mar 31, 2026
dcc21b4
Revert inadvertent committed files
ryantrem Mar 31, 2026
48b237c
Add stats
ryantrem Mar 31, 2026
eddfee6
Add perf trace commands
ryantrem Mar 31, 2026
97f1ee1
Make StartInspectable ref counted
ryantrem Mar 31, 2026
70903b5
Add ServiceContainer parent support, and use it to have a longer live…
ryantrem Mar 31, 2026
c782926
Add toolbar icon for cli connection status
ryantrem Mar 31, 2026
d394f25
Colored buttons
ryantrem Mar 31, 2026
db2374d
Wait for connection if needed
ryantrem Mar 31, 2026
c396381
Add get-shader-code
ryantrem Mar 31, 2026
df0816f
Update readme files
ryantrem Mar 31, 2026
a9af16a
Update package-lock.json
ryantrem Mar 31, 2026
dcd4983
Simplify CLI top level params
ryantrem Mar 31, 2026
682511f
Cleanup
ryantrem Mar 31, 2026
8dc50ac
Merge master and fix issues from TypeScript 6 compiler
ryantrem Apr 1, 2026
d28d073
Fix bridge ws imports after ts 6 fixes
ryantrem Apr 1, 2026
b28a9da
Cleanup WebSocket imports more
ryantrem Apr 1, 2026
77933e8
Move ugly WebSocket imports into webSocket.ts
ryantrem Apr 1, 2026
3006b0b
Merge remote-tracking branch 'origin/master' into inspector-v2/cli
ryantrem Apr 1, 2026
74b259a
Remove plan
ryantrem Apr 1, 2026
0055803
Add cli unit tests
ryantrem Apr 1, 2026
bf8f653
ServiceContainer unit tests
ryantrem Apr 1, 2026
4912ec8
Revert inadvertent files
ryantrem Apr 1, 2026
58e6661
Inspectable extensibility
ryantrem Apr 1, 2026
c4d95c3
Better errors for invalid sessions
ryantrem Apr 2, 2026
d0e3d0c
Add @experimental doc flags
ryantrem Apr 2, 2026
071dfc1
Merge master
ryantrem Apr 2, 2026
2d4c953
Update packages/dev/inspector-v2/src/services/cli/screenshotCommandSe…
ryantrem Apr 2, 2026
46aa208
PR feedback
ryantrem Apr 2, 2026
528fedc
PR feedback
ryantrem Apr 2, 2026
b006123
PR feedback
ryantrem Apr 2, 2026
b3405cb
PR feedback
ryantrem Apr 2, 2026
2a653e0
Merge remote-tracking branch 'ryantrem/inspector-v2/cli' into inspect…
ryantrem Apr 2, 2026
c401d19
Allow multiple calls to StartInspectable to add transient services
ryantrem Apr 8, 2026
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
29 changes: 28 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions packages/dev/core/src/Meshes/mesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4188,7 +4188,7 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData {
// Physics
//TODO implement correct serialization for physics impostors.
if (this.getScene()._getComponent(SceneComponentConstants.NAME_PHYSICSENGINE)) {
const impostor = this.getPhysicsImpostor();
const impostor = this.getPhysicsImpostor?.();
if (impostor) {
Comment thread
ryantrem marked this conversation as resolved.
serializationObject.physicsMass = impostor.getParam("mass");
serializationObject.physicsFriction = impostor.getParam("friction");
Expand Down Expand Up @@ -4234,7 +4234,7 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData {
// Physics
//TODO implement correct serialization for physics impostors.
if (this.getScene()._getComponent(SceneComponentConstants.NAME_PHYSICSENGINE)) {
const impostor = instance.getPhysicsImpostor();
const impostor = instance.getPhysicsImpostor?.();
if (impostor) {
serializationInstance.physicsMass = impostor.getParam("mass");
serializationInstance.physicsFriction = impostor.getParam("friction");
Expand Down
3 changes: 2 additions & 1 deletion packages/dev/inspector-v2/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"serve": "webpack serve --mode development",
"watch": "tsc -b tsconfig.build.json -w",
"watch:dev": "npm run watch",
"makeAvatar": "node scripts/makeAvatar.mjs"
"makeAvatar": "node scripts/makeAvatar.mjs",
"babylon-inspector": "node --no-warnings dist/cli/cli.js --bridge-script dist/cli/bridge.js"
},
"devDependencies": {
"@dev/addons": "1.0.0",
Expand Down
258 changes: 258 additions & 0 deletions packages/dev/inspector-v2/src/cli/bridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
/* eslint-disable no-console */
import { fileURLToPath } from "url";
import { type WebSocket, WebSocketServer } from "./webSocket.js";
import { LoadConfig } from "./config.js";
import { type BrowserRequest, type BrowserResponse, type CliRequest, type CliResponse, type SessionInfo } from "./protocol.js";

interface ISession extends SessionInfo {
/** The WebSocket connection for this session. */
ws: WebSocket;
}

/**
* Configuration for starting the bridge.
*/
export interface IBridgeConfig {
/** WebSocket port for browser sessions. Use 0 for OS-assigned port. */
browserPort: number;
/** WebSocket port for CLI connections. Use 0 for OS-assigned port. */
cliPort: number;
/** Timeout in ms for waiting for an initial session on a sessions request. Defaults to 5000. */
sessionWaitTimeoutMs?: number;
}

/**
* Handle returned by {@link startBridge} to control and inspect the running bridge.
*/
export interface IBridgeHandle {
/** The actual port the browser WebSocket server is listening on. */
browserPort: number;
/** The actual port the CLI WebSocket server is listening on. */
cliPort: number;
/** Shuts down the bridge, closing all connections and servers. */
shutdown: () => void;
}

/**
* Starts the Inspector bridge with the given configuration.
* @param config The ports to listen on. Use port 0 for OS-assigned ports.
* @returns A promise that resolves with a handle to control the running bridge.
*/
export async function StartBridge(config: IBridgeConfig): Promise<IBridgeHandle> {
let nextSessionId = 1;
const sessions = new Map<number, ISession>();
const pendingBrowserRequests = new Map<string, (response: string) => void>();
const sessionAddedListeners: (() => void)[] = [];
let requestCounter = 0;

function generateRequestId(): string {
return `bridge-req-${++requestCounter}`;
}

const sessionWaitTimeout = config.sessionWaitTimeoutMs ?? 5000;

async function waitForSession(): Promise<void> {
if (sessions.size > 0) {
return;
}
return await new Promise<void>((resolve) => {
const timer = setTimeout(() => {
const index = sessionAddedListeners.indexOf(listener);
if (index !== -1) {
sessionAddedListeners.splice(index, 1);
}
resolve();
}, sessionWaitTimeout);

const listener = () => {
clearTimeout(timer);
const index = sessionAddedListeners.indexOf(listener);
if (index !== -1) {
sessionAddedListeners.splice(index, 1);
}
resolve();
};
sessionAddedListeners.push(listener);
});
}

async function waitForBrowserResponse(requestId: string, timeoutMs = 30000): Promise<string> {
return await new Promise((resolve, reject) => {
const timer = setTimeout(() => {
pendingBrowserRequests.delete(requestId);
reject(new Error("Timeout"));
}, timeoutMs);

pendingBrowserRequests.set(requestId, (response) => {
clearTimeout(timer);
resolve(response);
});
});
}

// Browser-facing WebSocket server.
const browserWss = new WebSocketServer({ host: "127.0.0.1", port: config.browserPort });

// CLI-facing WebSocket server.
const cliWss = new WebSocketServer({ host: "127.0.0.1", port: config.cliPort });

browserWss.on("connection", (socket) => {
let session: ISession | null = null;

socket.on("message", (data) => {
let message: BrowserRequest;
try {
message = JSON.parse(data.toString());
} catch {
return;
}

switch (message.type) {
case "register": {
const id = nextSessionId++;
session = {
id,
name: message.name,
connectedAt: new Date().toISOString(),
ws: socket,
};
sessions.set(id, session);
console.log(`Session ${id} registered: "${session.name}"`);
for (const listener of sessionAddedListeners.splice(0)) {
listener();
}
break;
}
case "commandListResponse":
case "commandResponse": {
// Forward response back to the CLI that requested it.
const resolve = pendingBrowserRequests.get(message.requestId);
if (resolve) {
pendingBrowserRequests.delete(message.requestId);
resolve(JSON.stringify(message));
}
break;
}
}
});

socket.on("close", () => {
if (session) {
console.log(`Session ${session.id} disconnected: "${session.name}"`);
sessions.delete(session.id);
}
});
});

cliWss.on("connection", (socket) => {
socket.on("message", async (data) => {
let message: CliRequest;
try {
message = JSON.parse(data.toString());
} catch {
return;
}

function sendCliResponse(response: CliResponse) {
socket.send(JSON.stringify(response));
}

function sendBrowserRequest(target: ISession, request: BrowserResponse) {
target.ws.send(JSON.stringify(request));
}

switch (message.type) {
case "sessions": {
// Wait for at least one session to connect before responding.
await waitForSession();
const sessionList: SessionInfo[] = Array.from(sessions.values()).map((s) => ({
id: s.id,
name: s.name,
connectedAt: s.connectedAt,
}));
sendCliResponse({ type: "sessionsResponse", sessions: sessionList });
break;
}
case "commands": {
const session = sessions.get(message.sessionId);
if (!session) {
sendCliResponse({ type: "commandsResponse", error: `No session with id ${message.sessionId}` });
break;
}
const requestId = generateRequestId();
sendBrowserRequest(session, { type: "listCommands", requestId });
try {
const response = await waitForBrowserResponse(requestId);
socket.send(response);
} catch {
sendCliResponse({ type: "commandsResponse", error: "Timeout waiting for browser response" });
}
break;
}
case "exec": {
const session = sessions.get(message.sessionId);
if (!session) {
sendCliResponse({ type: "execResponse", error: `No session with id ${message.sessionId}` });
break;
}
const requestId = generateRequestId();
sendBrowserRequest(session, {
type: "execCommand",
requestId,
commandId: message.commandId,
args: message.args,
});
try {
const response = await waitForBrowserResponse(requestId);
socket.send(response);
} catch {
sendCliResponse({ type: "execResponse", error: "Timeout waiting for browser response" });
}
break;
}
case "stop": {
sendCliResponse({ type: "stopResponse", success: true });
shutdown();
break;
}
}
});
});

function shutdown(): void {
console.log("Inspector bridge shutting down.");

for (const session of sessions.values()) {
session.ws.close();
}
sessions.clear();

browserWss.close();
cliWss.close();
}

// Wait for both servers to be listening before returning.
await Promise.all([new Promise<void>((resolve) => browserWss.on("listening", resolve)), new Promise<void>((resolve) => cliWss.on("listening", resolve))]);

const actualBrowserPort = (browserWss.address() as import("net").AddressInfo).port;
const actualCliPort = (cliWss.address() as import("net").AddressInfo).port;

console.log(`Inspector bridge started.`);
console.log(` Browser port: ${actualBrowserPort}`);
console.log(` CLI port: ${actualCliPort}`);

return {
browserPort: actualBrowserPort,
cliPort: actualCliPort,
shutdown,
};
}

// Auto-start when run directly (not when imported for testing).
if (process.argv[1] === fileURLToPath(import.meta.url)) {
void (async () => {
const handle = await StartBridge(LoadConfig());
process.on("SIGTERM", () => handle.shutdown());
process.on("SIGINT", () => handle.shutdown());
})();
}
Loading
Loading