Compare commits

...

14 Commits

Author SHA1 Message Date
Colin McDonnell
7c9352392d Update readme and regen 2023-06-09 19:21:49 -07:00
Jarred Sumner
fef9853d5a wip 2023-06-05 02:02:13 -07:00
Jarred Sumner
7dae496847 wip 2023-06-04 19:59:22 -07:00
Jarred Sumner
92b060c6e2 fix some CLI things 2023-06-04 19:06:02 -07:00
Jarred Sumner
2a64e8b3bb fixup 2023-06-04 19:05:42 -07:00
Jarred Sumner
fde3b7fbb6 Fix build 2023-06-04 18:54:26 -07:00
Ashcon Partovi
c16e769383 Add types and sample heapsnapshot for JSC and V8 2023-06-04 18:54:26 -07:00
Ashcon Partovi
5badc728d0 Add a DevTools client 2023-06-04 18:54:26 -07:00
Jarred Sumner
c4b3b321c2 slightly more progress 2023-06-04 18:54:26 -07:00
Jarred Sumner
cf599e77d9 Fix C++ compile errors 2023-06-04 18:54:26 -07:00
Jarred Sumner
b727689a9b pushing this but it doesn't fix anything 2023-06-04 18:54:26 -07:00
Ashcon Partovi
4a36470588 Add script to generate DevTools protocol types for JSC and V8 2023-06-04 18:54:26 -07:00
Jarred Sumner
ca08cf6b0a Update BunInspector.cpp 2023-06-04 18:54:26 -07:00
Jarred Sumner
10bd0fac3a WIP support inspector 2023-06-04 18:54:26 -07:00
39 changed files with 55367 additions and 95 deletions

View File

@@ -19,11 +19,11 @@
"${workspaceFolder}/src/js/out",
"${workspaceFolder}/src/deps/boringssl/include/",
"${workspaceFolder}/src/deps",
"${workspaceFolder}/src/deps/uws/uSockets/src"
"${workspaceFolder}/src/deps/uws/uSockets/src",
"${workspaceFolder}/src/deps/uws/src"
],
"browse": {
"path": [
"${workspaceFolder}/../webkit-build/include/",
"${workspaceFolder}/bun-webkit/include/",
"${workspaceFolder}/src/bun.js/WebKit/WebKitBuild/Release/",
"${workspaceFolder}/src/bun.js/WebKit/WebKitBuild/Release/ICU/Headers/",
@@ -39,7 +39,8 @@
"${workspaceFolder}/src/bun.js/modules/*",
"${workspaceFolder}/src/deps",
"${workspaceFolder}/src/deps/boringssl/include/",
"${workspaceFolder}/src/deps/uws/uSockets/src"
"${workspaceFolder}/src/deps/uws/uSockets/src",
"${workspaceFolder}/src/deps/uws/src"
],
"limitSymbolsToIncludedHeaders": true,
"databaseFilename": ".vscode/cppdb"

14
.vscode/launch.json generated vendored
View File

@@ -136,6 +136,20 @@
"initCommands": ["process handle -p false -s false -n false SIGHUP"],
"console": "internalConsole"
},
{
"type": "lldb",
"request": "launch",
"name": "bun run [Inspect]",
"program": "bun-debug",
"args": ["--inspect-brk", "${file}"],
"cwd": "${fileDirname}",
"env": {
"FORCE_COLOR": "1",
"BUN_DEBUG_QUIET_LOGS": "1"
},
"initCommands": ["process handle -p false -s false -n false SIGHUP"],
"console": "internalConsole"
},
{
"type": "lldb",
"request": "launch",

169
packages/bun-devtools/.gitignore vendored Normal file
View File

@@ -0,0 +1,169 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
\*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
\*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
\*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
\*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.\*

View File

@@ -0,0 +1,3 @@
# bun-devtools
A set of auto-generated TypeScript types for the WebKit debugger protocol.

BIN
packages/bun-devtools/bun.lockb Executable file

Binary file not shown.

File diff suppressed because one or more lines are too long

14
packages/bun-devtools/heap/jsc.d.ts vendored Normal file
View File

@@ -0,0 +1,14 @@
export namespace JSC {
/**
* @link https://github.com/WebKit/webkit/blob/main/Source/JavaScriptCore/heap/HeapSnapshotBuilder.h
*/
export type HeapSnapshot = {
version: 2;
type: "Inspector";
nodes: number[];
nodeClassNames: string[];
edges: number[];
edgeTypes: string[];
edgeNames: string[];
};
}

30
packages/bun-devtools/heap/v8.d.ts vendored Normal file
View File

@@ -0,0 +1,30 @@
export namespace V8 {
/**
* @link https://github.com/julianburr/chrome-heap-snapshot-parser/blob/master/index.js#L72
* @link https://stackoverflow.com/questions/69802133/chrome-heap-snapshot-structure-explanation
*/
export type HeapSnapshot = {
snapshot: {
meta: {
node_fields: string[];
node_types: [string[], ...string[]]; // ?
edge_fields: string[];
edge_types: [string[], ...string[]]; // ?
trace_function_info_fields: string[];
trace_node_fields: string[];
sample_fields: string[];
location_fields: string[];
node_count: number;
edge_count: number;
trace_function_count: number;
};
};
nodes: number[];
edges: number[];
trace_tree: unknown[];
trace_function_infos: unknown[];
samples: unknown[];
locations: number[];
strings: string[];
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
export * from "./protocol/jsc";
export * from "./protocol/v8";

View File

@@ -0,0 +1,24 @@
{
"name": "bun-devtools",
"module": "./index.ts",
"version": "0.0.2",
"type": "module",
"exports": {
".": {
"import": "./index.ts",
"require": "./index.ts"
}
},
"scripts": {
"generate-protocol": "bun run scripts/generate-protocol.ts"
},
"files": [
"index.ts",
"package.json",
"tsconfig.json",
"protocol"
],
"peerDependencies": {
"typescript": "^5.0.0"
}
}

1953
packages/bun-devtools/protocol/jsc.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

3422
packages/bun-devtools/protocol/v8.d.ts vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,129 @@
// A DevTools client for JavaScriptCore.
import type { JSC } from "..";
type ClientOptions = {
url: string | URL;
event?: (event: JSC.Event<keyof JSC.EventMap>) => void;
request?: (request: JSC.Request<keyof JSC.RequestMap>) => void;
response?: (response: JSC.Response<keyof JSC.ResponseMap>) => void;
};
class Client {
#webSocket: WebSocket;
#requestId: number;
#pendingMessages: string[];
#pendingRequests: Map<number, AbortController>;
#ready: Promise<void>;
constructor(options: ClientOptions) {
this.#webSocket = new WebSocket(options.url);
this.#requestId = 1;
this.#pendingMessages = [];
this.#pendingRequests = new Map();
this.#ready = new Promise((resolve, reject) => {
this.#webSocket.addEventListener("open", () => {
for (const message of this.#pendingMessages) {
this.#send(message);
}
this.#pendingMessages.length = 0;
resolve();
});
this.#webSocket.addEventListener("message", ({ data }) => {
let response;
try {
response = { ...JSON.parse(data) };
} catch {
console.error("Received an invalid message:", data);
return;
}
const { id, error, result, method, params } = response;
if (method && params) {
options.event?.(response);
} else if (id && (result || error)) {
try {
options.response?.(response);
} finally {
const abort = this.#pendingRequests.get(id ?? -1);
if (!abort) {
console.error("Received an unexpected message:", response);
return;
}
if (error) {
abort.abort(new Error(JSON.stringify(error)));
} else {
abort.abort(result);
}
}
} else {
console.error("Received an unexpected message:", response);
}
});
this.#webSocket.addEventListener("error", (error) => {
reject(error);
});
this.#webSocket.addEventListener("close", ({ code, reason = ""}) => {
reject(new Error(`WebSocket closed: ${code} ${reason}`.trimEnd()));
});
});
}
get ready(): Promise<void> {
return this.#ready;
}
#send(message: string): void {
const { readyState } = this.#webSocket;
if (readyState === WebSocket.OPEN) {
this.#webSocket.send(message);
} else if (readyState === WebSocket.CONNECTING) {
this.#pendingMessages.push(message);
} else {
const closed = readyState === WebSocket.CLOSING ? "closing" : "closed";
throw new Error(`WebSocket is ${closed}`);
}
}
async fetch<T extends keyof JSC.RequestMap>(method: T, params: JSC.Request<T>["params"]): Promise<JSC.Response<T>> {
const request: JSC.Request<T> = {
id: this.#requestId++,
method,
params,
};
return new Promise((resolve, reject) => {
const abort = new AbortController();
abort.signal.addEventListener("abort", () => {
this.#pendingRequests.delete(request.id);
const { reason } = abort.signal;
if (reason instanceof Error) {
reject(reason);
} else {
resolve(reason);
}
});
this.#pendingRequests.set(request.id, abort);
this.#send(JSON.stringify(request));
});
}
}
const client = new Client({
url: "ws://localhost:9229",
event: (event) => console.log("EVENT:", event),
request: (request) => console.log("REQUEST:", request),
response: (response) => console.log("RESPONSE:", response),
});
await client.ready;
while (true) {
const [method, ...param] = prompt(">")?.split(" ") ?? [];
if (!method.trim()) {
continue;
}
const params = !param?.length ? {} : JSON.parse(eval(`JSON.stringify(${param.join(" ")})`));
try {
await client.fetch(method.trim() as any, params);
} catch (error) {
console.error(error);
}
}

View File

@@ -0,0 +1,255 @@
import { join } from "node:path";
import { writeFileSync, mkdirSync } from "node:fs";
import { spawnSync } from "node:child_process";
async function download<V>(url: string): Promise<V> {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`${response.status}: ${url}`);
}
return response.json();
}
type Protocol = {
name: string;
version: {
major: number;
minor: number;
};
domains: Domain[];
};
type Domain = {
domain: string;
types: Property[];
commands?: {
name: string;
description?: string;
parameters?: Property[];
returns?: Property[];
}[];
events?: {
name: string;
description?: string;
parameters: Property[];
}[];
};
type Property = {
id?: string;
type?: string;
name?: string;
description?: string;
optional?: boolean;
} & (
| {
type: "array";
items?: Property;
}
| {
type: "object";
properties?: Property[];
}
| {
type: "string";
enum?: string[];
}
| {
$ref: string;
}
);
function format(property: Property): string {
if (property.id) {
const comment = property.description
? `/** ${property.description} */\n`
: "";
const body = format({ ...property, id: undefined });
return `${comment}export type ${property.id} = ${body};\n`;
}
if (property.type === "array") {
const type = "items" in property ? format(property.items!) : "unknown";
return `Array<${type}>`;
}
if (property.type === "object") {
if (!("properties" in property)) {
return "Record<string, unknown>";
}
if (property.properties!.length === 0) {
return "{}";
}
const properties = property
.properties!.map((property) => {
const comment = property.description
? `/** ${property.description} */\n`
: "";
const name = `${property.name}${property.optional ? "?" : ""}`;
return `${comment} ${name}: ${format(property)};`;
})
.join("\n");
return `{\n${properties}}`;
}
if (property.type === "string") {
if (!("enum" in property)) {
return "string";
}
return property.enum!.map((v) => `"${v}"`).join(" | ");
}
if ("$ref" in property) {
if (/^Page|DOM|Security|CSS|IO|Emulation\./.test(property.$ref)) {
return "unknown";
}
return property.$ref;
}
if (property.type === "integer") {
return "number";
}
return property.type;
}
function formatAll(protocol: Protocol): string {
let body = "";
const append = (property: Property) => {
body += format(property);
};
const titlize = (name: string) =>
name.charAt(0).toUpperCase() + name.slice(1);
const events = new Map();
const commands = new Map();
for (const domain of protocol.domains) {
body += `export namespace ${domain.domain} {`;
for (const type of domain.types ?? []) {
append(type);
}
for (const event of domain.events ?? []) {
const symbol = `${domain.domain}.${event.name}`;
const title = titlize(event.name);
events.set(symbol, `${domain.domain}.${title}`);
append({
id: `${title}Event`,
type: "object",
description: `\`${symbol}\``,
properties: event.parameters ?? [],
});
}
for (const command of domain.commands ?? []) {
const symbol = `${domain.domain}.${command.name}`;
const title = titlize(command.name);
commands.set(symbol, `${domain.domain}.${title}`);
append({
id: `${title}Request`,
type: "object",
description: `\`${symbol}\``,
properties: command.parameters ?? [],
});
append({
id: `${title}Response`,
type: "object",
description: `\`${symbol}\``,
properties: command.returns ?? [],
});
}
body += "};";
}
for (const type of ["Event", "Request", "Response"]) {
const source = type === "Event" ? events : commands;
append({
id: `${type}Map`,
type: "object",
properties: [...source.entries()].map(([name, title]) => ({
name: `"${name}"`,
$ref: `${title}${type}`,
})),
});
}
body += `export type Event<T extends keyof EventMap> = {
method: T;
params: EventMap[T];
};
export type Request<T extends keyof RequestMap> = {
id: number;
method: T;
params: RequestMap[T];
};
export type Response<T extends keyof ResponseMap> = {
id: number;
} & ({
method?: T;
result: ResponseMap[T];
} | {
error: {
code?: string;
message: string;
};
});`;
return `export namespace ${protocol.name.toUpperCase()} {${body}};`;
}
async function downloadV8(): Promise<Protocol> {
const baseUrl =
"https://raw.githubusercontent.com/ChromeDevTools/devtools-protocol/master/json";
const filter = [
"Runtime",
"Network",
"Console",
"Debugger",
"Profiler",
"HeapProfiler",
];
return Promise.all([
download<Protocol>(`${baseUrl}/js_protocol.json`),
download<Protocol>(`${baseUrl}/browser_protocol.json`),
]).then(([js, browser]) => ({
name: "v8",
version: js.version,
domains: [...js.domains, ...browser.domains]
.filter((domain) => filter.includes(domain.domain))
.sort((a, b) => a.domain.localeCompare(b.domain)),
}));
}
async function downloadJsc(): Promise<Protocol> {
const baseUrl =
"https://raw.githubusercontent.com/WebKit/WebKit/main/Source/JavaScriptCore/inspector/protocol";
return {
name: "jsc",
version: {
major: 1,
minor: 3,
},
domains: await Promise.all([
download<Domain>(`${baseUrl}/Debugger.json`),
download<Domain>(`${baseUrl}/Heap.json`),
download<Domain>(`${baseUrl}/ScriptProfiler.json`),
download<Domain>(`${baseUrl}/Runtime.json`),
download<Domain>(`${baseUrl}/Network.json`),
download<Domain>(`${baseUrl}/Console.json`),
download<Domain>(`${baseUrl}/GenericTypes.json`),
]).then((domains) =>
domains.sort((a, b) => a.domain.localeCompare(b.domain))
),
};
}
async function run(cwd: string) {
const [jsc, v8] = await Promise.all([downloadJsc(), downloadV8()]);
try {
mkdirSync(cwd);
} catch (error) {
if (error.code !== "EEXIST") {
throw error;
}
}
const write = (name: string, data: string) => {
writeFileSync(join(cwd, name), data);
spawnSync("bunx", ["prettier", "--write", name], { cwd, stdio: "ignore" });
};
// Note: Can be uncommented to inspect the JSON protocol files.
// write("devtools/jsc.json", JSON.stringify(jsc));
// write("devtools/v8.json", JSON.stringify(v8));
write("jsc.d.ts", "// GENERATED - DO NOT EDIT\n" + formatAll(jsc));
write("v8.d.ts", "// GENERATED - DO NOT EDIT\n" + formatAll(v8));
}
run(join(__dirname, "..", "protocol"))
.catch(console.error);

View File

@@ -0,0 +1,18 @@
{
"compilerOptions": {
"lib": ["ESNext"],
"module": "esnext",
"target": "esnext",
"moduleResolution": "bundler",
"moduleDetection": "force",
"allowImportingTsExtensions": true,
"strict": true,
"downlevelIteration": true,
"skipLibCheck": true,
"jsx": "preserve",
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"allowJs": true,
"noEmit": true
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright (C) 2014 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#if ENABLE(INSPECTOR_ALTERNATE_DISPATCHERS)
#include <JavaScriptCore/AugmentableInspectorControllerClient.h>
#include <JavaScriptCore/InspectorBackendDispatcher.h>
#include <JavaScriptCore/InspectorFrontendRouter.h>
namespace Inspector {
class InspectorAgentBase;
class AugmentableInspectorController {
public:
virtual ~AugmentableInspectorController() {}
virtual AugmentableInspectorControllerClient* augmentableInspectorControllerClient() const = 0;
virtual void setAugmentableInspectorControllerClient(AugmentableInspectorControllerClient*) = 0;
virtual const FrontendRouter& frontendRouter() const = 0;
virtual BackendDispatcher& backendDispatcher() = 0;
virtual void registerAlternateAgent(std::unique_ptr<InspectorAgentBase>) = 0;
bool connected() const { return frontendRouter().hasFrontends(); }
};
} // namespace Inspector
#endif // ENABLE(INSPECTOR_ALTERNATE_DISPATCHERS)

View File

@@ -0,0 +1,391 @@
#include "BunInspector.h"
#include <JavaScriptCore/Heap.h>
#include <JavaScriptCore/JSGlobalObject.h>
#include <JavaScriptCore/JSGlobalObjectDebugger.h>
namespace Zig {
WTF_MAKE_ISO_ALLOCATED_IMPL(BunInspector);
extern "C" void Bun__waitForDebuggerToStart();
extern "C" void Bun__debuggerIsReady();
static BunInspector* inspectorFromGlobal(Zig::GlobalObject* globalObject)
{
RELEASE_ASSERT(globalObject->bunInspectorPtr);
return reinterpret_cast<BunInspector*>(globalObject->bunInspectorPtr);
}
class BunInspectorConnection {
public:
WTF::Deque<WTF::CString> m_messages;
RefPtr<BunInspector> inspector;
bool hasSentWelcomeMessage = false;
BunInspectorConnection(RefPtr<BunInspector> inspector)
: inspector(inspector)
, m_messages()
{
}
};
void BunInspector::sendMessageToFrontend(const String& message)
{
String out = message;
auto jsonObject = WTF::JSONImpl::Value::parseJSON(message);
if (jsonObject) {
if (auto object = jsonObject->asObject()) {
auto method = object->getString("method"_s);
// {
// "scriptId": "384",
// "url": "file:///private/tmp/empty.js",
// "startLine": 0,
// "startColumn": 0,
// "endLine": 1,
// "endColumn": 0,
// "executionContextId": 1,
// "hash": "a3b314362f7e47deabee6100e0d8081619194faf1b5741e0fe2f88b150557ddd",
// "executionContextAuxData": { "isDefault": true },
// "isLiveEdit": false,
// "sourceMapURL": "",
// "hasSourceURL": false,
// "isModule": false,
// "length": 21,
// "stackTrace": {
// "callFrames": [
// {
// "functionName": "internalCompileFunction",
// "scriptId": "62",
// "url": "node:internal/vm",
// "lineNumber": 72,
// "columnNumber": 17
// }
// ]
// },
// "scriptLanguage": "JavaScript",
// "embedderName": "file:///private/tmp/empty.js"
// }
if (method == "Debugger.scriptParsed"_s) {
if (auto params = object->getObject("params"_s)) {
params->setInteger("executionContextId"_s, 1);
auto url = makeString("file://"_s, params->getString("url"_s));
params->setString("url"_s, url);
// TODO: content hash
params->setInteger("hash"_s, url.hash());
params->setBoolean("isModule"_s, true);
params->setString("scriptLanguage"_s, "JavaScript"_s);
params->setString("embedderName"_s, "Bun!"_s);
}
out = object->toJSONString();
}
if (method == "Debugger.enable"_s) {
// debuggerId is missing from the response
auto params = WTF::JSONImpl::Object::create();
params->setString("debuggerId"_s, "3701622443570787625.-8711178633418819848"_s);
object->setObject("params"_s, WTFMove(params));
}
out = object->toJSONString();
}
}
{
LockHolder locker(this->m_pendingMessagesLock);
if (!this->hasSentWelcomeMessage) {
this->hasSentWelcomeMessage = true;
auto welcomeMessage = makeString(
"{ \"method\": \"Runtime.executionContextCreated\", \"params\":{\"context\":{\"id\":"_s,
this->scriptExecutionContext()->identifier(),
",\"origin\":\"\",\"name\":\""_s,
this->identifier(),
"\",\"uniqueId\":\"1234\",\"auxData\":{\"isDefault\":true}}}}"_s);
this->m_pendingMessages.append(WTFMove(welcomeMessage.isolatedCopy()));
}
this->m_pendingMessages.append(WTFMove(out.isolatedCopy()));
}
us_wakeup_loop((us_loop_t*)this->loop);
}
void BunInspector::drainIncomingMessages()
{
LockHolder locker(this->m_incomingMessagesLock);
size_t size = this->m_incomingMessages.size();
while (size > 0) {
auto& message = this->m_incomingMessages.first();
this->sendMessageToTargetBackend(message);
this->m_incomingMessages.removeFirst();
size = this->m_incomingMessages.size();
}
}
void BunInspector::didParseSource(SourceID id, const Debugger::Script& script)
{
}
void BunInspector::drainOutgoingMessages()
{
if (this->server->numSubscribers("BunInspectorConnection") == 0) {
return;
}
LockHolder locker(this->m_pendingMessagesLock);
size_t size = this->m_pendingMessages.size();
while (size > 0) {
auto& message = this->m_pendingMessages.first();
auto utf8 = message.utf8();
std::string_view view { utf8.data(), utf8.length() };
if (!this->server->publish("BunInspectorConnection", view, uWS::OpCode::TEXT, false)) {
return;
}
this->m_pendingMessages.removeFirst();
size = this->m_pendingMessages.size();
}
}
extern "C" void Bun__tickWhileWaitingForDebugger(JSC::JSGlobalObject* globalObject);
void BunInspector::startServer(WTF::String hostname, uint16_t port, WTF::URL url, WTF::String title)
{
uWS::App* app = new uWS::App();
this->server = app;
this->loop = uWS::Loop::get();
auto host = hostname.utf8();
// https://chromedevtools.github.io/devtools-protocol/ GET /json or /json/list
app->get("/json", [hostname, port, url, title = title, inspector = this](auto* res, auto* /*req*/) {
auto identifier = inspector->identifier();
auto jsonString = makeString(
"[ {\"faviconUrl\": \"https://bun.sh/favicon.svg\", \"description\": \"\", \"devtoolsFrontendUrl\": \"devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws="_s,
hostname,
":"_s,
port,
"/devtools/page/"_s,
identifier,
"\","_s
" \"id\": \"6e99c4f9-6bb6-4f45-9749-5772545b2371\","_s,
" \"title\": \""_s,
title,
"\","
" \"type\": \"node\","_s,
" \"url\": \"file://"_s,
identifier,
"\","_s
" \"webSocketDebuggerUrl\": \"ws://"_s,
hostname,
":"_s,
port,
"/devtools/page/"_s,
identifier,
"\"} ]"_s);
auto utf8 = jsonString.utf8();
res->writeStatus("200 OK");
res->writeHeader("Content-Type", "application/json");
res->end(utf8.data(), utf8.length());
})
.get("/json/version", [](auto* res, auto* req) {
auto out = makeString("{\"Browser\": \"Bun/"_s, Bun__version, "\",\"Protocol-Version\": \"1.1\"}"_s);
auto utf8 = out.utf8();
res->writeStatus("200 OK");
res->writeHeader("Content-Type", "application/json");
res->end({ utf8.data(), utf8.length() });
})
.ws<BunInspectorConnection*>("/*", { /* Settings */
.compression = uWS::DISABLED,
.maxPayloadLength = 1024 * 1024 * 1024,
.idleTimeout = 512,
.maxBackpressure = 64 * 1024 * 1024,
.closeOnBackpressureLimit = false,
.resetIdleTimeoutOnSend = false,
.sendPingsAutomatically = true,
/* Handlers */
.upgrade = nullptr,
.open = [inspector = this](auto* ws) {
BunInspectorConnection** connectionPtr = ws->getUserData();
*connectionPtr = new BunInspectorConnection(inspector);
ws->subscribe("BunInspectorConnection");
BunInspectorConnection* connection = *connectionPtr;
Bun__debuggerIsReady(); },
.message = [inspector = this](auto* ws, std::string_view message, uWS::OpCode opCode) {
if (opCode == uWS::OpCode::TEXT) {
if (!inspector) {
ws->close();
return;
}
BunInspectorConnection** connectionPtr = ws->getUserData();
BunInspectorConnection* connection = *connectionPtr;
connection->inspector->dispatchToBackend(message);
connection->inspector->drainOutgoingMessages();
} },
.drain = [](auto* ws) {
/* Check ws->getBufferedAmount() here */
BunInspectorConnection** connectionPtr = ws->getUserData();
BunInspectorConnection* connection = *connectionPtr;
if (!connection) {
return;
}
while (connection->m_messages.size() > 0) {
auto& message = connection->m_messages.first();
std::string_view view { message.data(), message.length() };
if (!ws->send(view, uWS::OpCode::TEXT, false, false)) {
return;
}
connection->m_messages.removeFirst();
}
connection->inspector->drainOutgoingMessages(); },
.ping = [](auto* /*ws*/, std::string_view) {
/* Not implemented yet */ },
.pong = [](auto* /*ws*/, std::string_view) {
/* Not implemented yet */ },
.close = [](auto* ws, int /*code*/, std::string_view /*message*/) {
BunInspectorConnection** connectionPtr = ws->getUserData();
BunInspectorConnection* connection = *connectionPtr;
if (!connection) {
return;
}
if (connection->inspector.get()) {
connection->inspector->disconnect();
connection->inspector = nullptr;
}
connection->m_messages.clear();
delete connection; } })
.any("/*", [](auto* res, auto* req) {
res->writeStatus("404 Not Found");
res->writeHeader("Content-Type", "text/plain");
res->write(req->getUrl());
res->end(" was not found");
})
.listen(std::string(host.data(), host.length()), port, [inspector = this](auto* listen_socket) {
inspector->loop->addPostHandler(inspector, [inspector = inspector](uWS::Loop* loop) {
inspector->drainOutgoingMessages();
});
WebCore::ScriptExecutionContext::postTaskTo(
inspector->scriptExecutionContext()->identifier(),
[inspector = inspector](WebCore::ScriptExecutionContext& ctx) mutable {
inspector->readyToStartDebugger();
});
inspector->loop->run();
});
}
void BunInspector::readyToStartDebugger()
{
this->ensureDebugger();
auto& inspectorController = globalObject()->inspectorController();
auto* debugger = inspectorController.debugger();
debugger->addObserver(*this);
debugger->schedulePauseAtNextOpportunity();
}
BunInspector* BunInspector::startWebSocketServer(
Zig::GlobalObject* globalObject,
WebCore::ScriptExecutionContext& context,
WTF::String hostname,
uint16_t port,
WTF::Function<void(BunInspector*, bool success)>&& callback)
{
context.ensureURL();
auto url = context.url();
auto identifier = url.fileSystemPath();
auto title = makeString(
url.fileSystemPath(),
" (Bun "_s, Bun__version, ")"_s);
auto* inspector = new BunInspector(&context, nullptr, WTFMove(identifier));
reinterpret_cast<Zig::GlobalObject*>(globalObject)->bunInspectorPtr = inspector;
auto backgroundThreadFunction = [inspector = inspector, hostname = hostname.isolatedCopy(), port = port, url = WTFMove(url), title = WTFMove(title)]() -> void {
inspector->startServer(hostname, port, url, WTFMove(title));
};
WTF::Thread::create("BunInspector", WTFMove(backgroundThreadFunction))->detach();
callback(inspector, true);
Bun__waitForDebuggerToStart();
return inspector;
}
void BunInspector::dispatchToBackend(std::string_view message)
{
WTF::CString data { message.data(), message.length() };
WTF::String msg = WTF::String::fromUTF8(data.data(), data.length());
bool needsTask = true;
{
LockHolder incomingMessagesLock(this->m_incomingMessagesLock);
needsTask = this->m_incomingMessages.isEmpty();
this->m_incomingMessages.append(WTFMove(msg.isolatedCopy()));
}
WebCore::ScriptExecutionContext::postTaskTo(
scriptExecutionContext()->identifier(),
[inspector = this](WebCore::ScriptExecutionContext& ctx) mutable {
inspector->drainIncomingMessages();
});
}
void BunInspector::sendMessageToTargetBackend(const WTF::String& message)
{
globalObject()->inspectorController().dispatchMessageFromFrontend(message);
}
void BunInspector::connect(Inspector::FrontendChannel::ConnectionType connectionType)
{
globalObject()->inspectorController().connectFrontend(*this, false, false);
}
void BunInspector::disconnect()
{
globalObject()->inspectorController().disconnectFrontend(*this);
}
void BunInspector::didPause(JSGlobalObject* jsGlobalObject, DebuggerCallFrame& callframe, JSValue exceptionOrCaughtValue)
{
printf("didPause\n");
}
void BunInspector::didContinue()
{
printf("didContinue\n");
}
void BunInspector::waitForMessages()
{
this->m_incomingMessagesLock.lock();
}
void BunInspector::ensureDebugger()
{
this->connect(Inspector::FrontendChannel::ConnectionType::Local);
auto* debugger = reinterpret_cast<Inspector::JSGlobalObjectDebugger*>(this->globalObject()->inspectorController().debugger());
debugger->runWhilePausedCallback = [](JSC::JSGlobalObject& globalObject, bool& isResumed) {
auto* inspector = inspectorFromGlobal(reinterpret_cast<Zig::GlobalObject*>(&globalObject));
while (!isResumed) {
inspector->drainIncomingMessages();
}
};
}
} // namespace Zig

View File

@@ -0,0 +1,104 @@
#pragma once
#include "root.h"
#include <uws/src/App.h>
#include <JavaScriptCore/InspectorTarget.h>
#include <JavaScriptCore/InspectorFrontendChannel.h>
#include "ContextDestructionObserver.h"
#include <wtf/RefPtr.h>
#include <JavaScriptCore/Debugger.h>
#include <wtf/Deque.h>
#include "JSGlobalObjectInspectorController.h"
namespace Zig {
using namespace JSC;
using namespace WebCore;
class BunInspector final : public RefCounted<BunInspector>, ::Inspector::InspectorTarget, ::Inspector::FrontendChannel, public WebCore::ContextDestructionObserver, JSC::Debugger::Observer {
public:
WTF_MAKE_ISO_ALLOCATED(BunInspector);
BunInspector(ScriptExecutionContext* context, uWS::App* server, WTF::String&& identifier)
: server(server)
, WebCore::ContextDestructionObserver(context)
, m_identifier(WTFMove(identifier))
{
}
public:
~BunInspector()
{
server->close();
}
bool isProvisional() const override { return false; }
String identifier() const override { return m_identifier; }
Inspector::InspectorTargetType type() const override { return Inspector::InspectorTargetType::DedicatedWorker; }
GlobalObject* globalObject() { return static_cast<GlobalObject*>(scriptExecutionContext()->jsGlobalObject()); }
void startServer(WTF::String hostname, uint16_t port, WTF::URL url, WTF::String title);
Lock m_mutex;
void ensureDebugger();
JSC::Debugger* debugger() { return globalObject()->inspectorController().debugger(); }
void didPause(JSGlobalObject*, DebuggerCallFrame&, JSValue /* exceptionOrCaughtValue */) override;
void didContinue() override;
void didParseSource(SourceID, const Debugger::Script&) override;
void failedToParseSource(const String& /* url */, const String& /* data */, int /* firstLine */, int /* errorLine */, const String& /* errorMessage */) override {}
void didCreateNativeExecutable(NativeExecutable&) override {}
void willCallNativeExecutable(CallFrame*) override {}
void willEnter(CallFrame*) override {}
void didQueueMicrotask(JSGlobalObject*, MicrotaskIdentifier) override {}
void willRunMicrotask(JSGlobalObject*, MicrotaskIdentifier) override {}
void didRunMicrotask(JSGlobalObject*, MicrotaskIdentifier) override {}
void applyBreakpoints(CodeBlock*) override {}
void breakpointActionLog(JSGlobalObject*, const String& /* data */) override {}
void breakpointActionSound(BreakpointActionID) override {}
void breakpointActionProbe(JSGlobalObject*, BreakpointActionID, unsigned /* batchId */, unsigned /* sampleId */, JSValue /* result */) override {}
void didDeferBreakpointPause(BreakpointID) override {}
static BunInspector* startWebSocketServer(
Zig::GlobalObject* globalObject,
WebCore::ScriptExecutionContext& ctx,
WTF::String hostname,
uint16_t port,
WTF::Function<void(BunInspector*, bool success)>&& callback);
// Connection management.
void connect(Inspector::FrontendChannel::ConnectionType) override;
void disconnect() override;
void sendMessageToTargetBackend(const String&) override;
bool hasConnectedFrontends() { return connectionCounter > 0; }
void sendMessageToFrontend(const String& message) override;
Inspector::FrontendChannel::ConnectionType connectionType() const override { return Inspector::FrontendChannel::ConnectionType::Remote; }
int connectionCounter = 0;
bool hasSentWelcomeMessage = false;
void drainOutgoingMessages();
void drainIncomingMessages();
void waitForMessages();
void readyToStartDebugger();
private:
void dispatchToBackend(std::string_view message);
WTF::String m_identifier;
WTF::Lock m_pendingMessagesLock;
uWS::App* server;
uWS::Loop* loop;
Deque<WTF::String> m_pendingMessages;
Deque<WTF::String> m_incomingMessages;
WTF::Lock m_incomingMessagesLock;
};
}

View File

@@ -34,6 +34,8 @@
using namespace JSC;
using namespace WTF;
extern "C" bool JSGlobalObject__startRemoteInspector(JSC::JSGlobalObject* globalObject, const char* host, uint16_t port);
JSC_DECLARE_HOST_FUNCTION(functionStartRemoteDebugger);
JSC_DEFINE_HOST_FUNCTION(functionStartRemoteDebugger, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
@@ -45,38 +47,13 @@ JSC_DEFINE_HOST_FUNCTION(functionStartRemoteDebugger, (JSGlobalObject * globalOb
JSC::JSValue hostValue = callFrame->argument(0);
JSC::JSValue portValue = callFrame->argument(1);
const char* host = defaultHost;
if (hostValue.isString()) {
auto str = hostValue.toWTFString(globalObject);
if (!str.isEmpty())
host = toCString(str).data();
} else if (!hostValue.isUndefined()) {
throwVMError(globalObject, scope, createTypeError(globalObject, "host must be a string"_s));
return JSC::JSValue::encode(JSC::jsUndefined());
}
bool res = JSGlobalObject__startRemoteInspector(
globalObject,
hostValue.isUndefinedOrNull() ? defaultHost : hostValue.toWTFString(globalObject).utf8().data(),
portValue.isUndefinedOrNull() ? defaultPort : portValue.toUInt32(globalObject));
uint16_t port = defaultPort;
if (portValue.isNumber()) {
auto port_int = portValue.toUInt32(globalObject);
if (!(port_int > 0 && port_int < 65536)) {
throwVMError(globalObject, scope, createRangeError(globalObject, "port must be between 0 and 65535"_s));
return JSC::JSValue::encode(JSC::jsUndefined());
}
port = port_int;
} else if (!portValue.isUndefined()) {
throwVMError(globalObject, scope, createTypeError(globalObject, "port must be a number between 0 and 65535"_s));
return JSC::JSValue::encode(JSC::jsUndefined());
}
globalObject->setInspectable(true);
auto& server = Inspector::RemoteInspectorServer::singleton();
if (!server.start(reinterpret_cast<const char*>(host), port)) {
throwVMError(globalObject, scope, createError(globalObject, "Failed to start server \""_s + host + ":"_s + port + "\". Is port already in use?"_s));
return JSC::JSValue::encode(JSC::jsUndefined());
}
RELEASE_AND_RETURN(scope, JSC::JSValue::encode(JSC::jsUndefined()));
RELEASE_AND_RETURN(scope, JSC::JSValue::encode(jsBoolean(res)));
#else
auto& vm = globalObject->vm();
auto scope = DECLARE_THROW_SCOPE(vm);

View File

@@ -0,0 +1,149 @@
/*
* Copyright (C) 2014, 2015 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <JavaScriptCore/InspectorAgentRegistry.h>
#include <JavaScriptCore/InspectorEnvironment.h>
#include <JavaScriptCore/InspectorFrontendRouter.h>
#include <JavaScriptCore/Strong.h>
#include <wtf/Forward.h>
#include <wtf/Noncopyable.h>
#include <wtf/text/WTFString.h>
#if ENABLE(INSPECTOR_ALTERNATE_DISPATCHERS)
#include <AugmentableInspectorController.h>
#endif
namespace JSC {
class CallFrame;
class ConsoleClient;
class Exception;
class JSGlobalObject;
}
namespace Inspector {
class BackendDispatcher;
class FrontendChannel;
class InjectedScriptManager;
class InspectorAgent;
class InspectorConsoleAgent;
class InspectorDebuggerAgent;
class InspectorScriptProfilerAgent;
class JSGlobalObjectConsoleClient;
class JSGlobalObjectDebugger;
class ScriptCallStack;
struct JSAgentContext;
class JSGlobalObjectInspectorController final
: public InspectorEnvironment
#if ENABLE(INSPECTOR_ALTERNATE_DISPATCHERS)
,
public AugmentableInspectorController
#endif
{
WTF_MAKE_NONCOPYABLE(JSGlobalObjectInspectorController);
WTF_MAKE_FAST_ALLOCATED;
public:
JSGlobalObjectInspectorController(JSC::JSGlobalObject&);
~JSGlobalObjectInspectorController() final;
void connectFrontend(FrontendChannel&, bool isAutomaticInspection, bool immediatelyPause);
void disconnectFrontend(FrontendChannel&);
void dispatchMessageFromFrontend(const String&);
void globalObjectDestroyed();
bool includesNativeCallStackWhenReportingExceptions() const { return m_includeNativeCallStackWithExceptions; }
void setIncludesNativeCallStackWhenReportingExceptions(bool includesNativeCallStack) { m_includeNativeCallStackWithExceptions = includesNativeCallStack; }
void reportAPIException(JSC::JSGlobalObject*, JSC::Exception*);
WeakPtr<JSC::ConsoleClient> consoleClient() const;
bool developerExtrasEnabled() const final;
bool canAccessInspectedScriptState(JSC::JSGlobalObject*) const final { return true; }
InspectorFunctionCallHandler functionCallHandler() const final;
InspectorEvaluateHandler evaluateHandler() const final;
void frontendInitialized() final;
WTF::Stopwatch& executionStopwatch() const final;
JSC::Debugger* debugger() final;
JSC::VM& vm() final;
#if ENABLE(INSPECTOR_ALTERNATE_DISPATCHERS)
AugmentableInspectorControllerClient* augmentableInspectorControllerClient() const final
{
return m_augmentingClient;
}
void setAugmentableInspectorControllerClient(AugmentableInspectorControllerClient* client) final { m_augmentingClient = client; }
const FrontendRouter& frontendRouter() const final { return m_frontendRouter.get(); }
BackendDispatcher& backendDispatcher() final { return m_backendDispatcher.get(); }
void registerAlternateAgent(std::unique_ptr<InspectorAgentBase>) final;
#endif
private:
void appendAPIBacktrace(ScriptCallStack&);
InspectorAgent& ensureInspectorAgent();
InspectorDebuggerAgent& ensureDebuggerAgent();
JSAgentContext jsAgentContext();
void createLazyAgents();
JSC::JSGlobalObject& m_globalObject;
std::unique_ptr<InjectedScriptManager> m_injectedScriptManager;
std::unique_ptr<JSGlobalObjectConsoleClient> m_consoleClient;
Ref<WTF::Stopwatch> m_executionStopwatch;
std::unique_ptr<JSGlobalObjectDebugger> m_debugger;
AgentRegistry m_agents;
InspectorConsoleAgent* m_consoleAgent { nullptr };
// Lazy, but also on-demand agents.
InspectorAgent* m_inspectorAgent { nullptr };
InspectorDebuggerAgent* m_debuggerAgent { nullptr };
Ref<FrontendRouter> m_frontendRouter;
Ref<BackendDispatcher> m_backendDispatcher;
// Used to keep the JSGlobalObject and VM alive while we are debugging it.
JSC::Strong<JSC::JSGlobalObject> m_strongGlobalObject;
RefPtr<JSC::VM> m_strongVM;
bool m_includeNativeCallStackWithExceptions { true };
bool m_isAutomaticInspection { false };
bool m_pauseAfterInitialization { false };
bool m_didCreateLazyAgents { false };
#if ENABLE(INSPECTOR_ALTERNATE_DISPATCHERS)
AugmentableInspectorControllerClient* m_augmentingClient { nullptr };
#endif
};
} // namespace Inspector

View File

@@ -7,6 +7,7 @@
#include "_libusockets.h"
extern "C" void Bun__startLoop(us_loop_t* loop);
extern "C" void Bun__getMainPath(JSC::JSGlobalObject* globalObject, ZigString* out);
namespace WebCore {
@@ -34,6 +35,18 @@ static void registerHTTPContextForWebSocket(ScriptExecutionContext* script, us_s
}
}
void ScriptExecutionContext::setURL(const ZigString* filePath)
{
setURL(WTF::URL::fileURLWithFileSystemPath(toStringCopy(*filePath)).isolatedCopy());
}
void ScriptExecutionContext::ensureURL()
{
ZigString filePath;
Bun__getMainPath(m_globalObject, &filePath);
setURL(&filePath);
}
us_socket_context_t* ScriptExecutionContext::webSocketContextSSL()
{
if (!m_ssl_client_websockets_ctx) {

View File

@@ -106,6 +106,11 @@ public:
void reportException(const String& errorMessage, int lineNumber, int columnNumber, const String& sourceURL, JSC::Exception* exception, RefPtr<void*>&&, CachedScript* = nullptr, bool = false)
{
}
void setURL(const WTF::URL& url) { m_url = url; }
void setURL(const ZigString* sourceFilePath);
void ensureURL();
// void reportUnhandledPromiseRejection(JSC::JSGlobalObject&, JSC::JSPromise&, RefPtr<Inspector::ScriptCallStack>&&)
// {
// }

View File

@@ -114,6 +114,8 @@
#include "JSEnvironmentVariableMap.h"
#include "DOMIsoSubspaces.h"
#include "BunInspector.h"
#if ENABLE(REMOTE_INSPECTOR)
#include "JavaScriptCore/RemoteInspectorServer.h"
#endif
@@ -238,10 +240,33 @@ extern "C" void JSCInitialize(const char* envp[], size_t envc, void (*onCrash)(c
}
}
extern "C" bool JSGlobalObject__startRemoteInspector(Zig::GlobalObject* globalObject, const char* host, uint16_t port)
{
#if !ENABLE(REMOTE_INSPECTOR)
return false;
#else
bool didSucceed = false;
// This function calls immediately.
auto inspector = BunInspector::startWebSocketServer(
globalObject,
*globalObject->scriptExecutionContext(),
WTF::String::fromUTF8(host),
port, [port, &didSucceed](BunInspector* inspector, bool success) {
didSucceed = success;
if (success) {
inspector->ref();
}
});
return didSucceed;
#endif
}
extern "C" void* Bun__getVM();
extern "C" JSC__JSGlobalObject* Zig__GlobalObject__create(JSClassRef* globalObjectClass, int count,
void* console_client)
void* console_client, int inspector)
{
auto heapSize = JSC::HeapType::Large;
@@ -259,9 +284,11 @@ extern "C" JSC__JSGlobalObject* Zig__GlobalObject__create(JSClassRef* globalObje
if (count > 0) {
globalObject->installAPIGlobals(globalObjectClass, count, vm);
}
if (inspector != 0) {
globalObject->setInspectable(true);
globalObject->inspectorController().debugger();
}
JSC::gcProtect(globalObject);
vm.ref();
return globalObject;
}
@@ -526,6 +553,9 @@ WebCore::ScriptExecutionContext* GlobalObject::scriptExecutionContext() const
void GlobalObject::reportUncaughtExceptionAtEventLoop(JSGlobalObject* globalObject,
JSC::Exception* exception)
{
if (globalObject->hasDebugger()) {
globalObject->debugger()->exception(globalObject, nullptr, exception, false);
}
Bun__reportUnhandledError(globalObject, JSValue::encode(JSValue(exception)));
}
@@ -537,10 +567,16 @@ void GlobalObject::promiseRejectionTracker(JSGlobalObject* obj, JSC::JSPromise*
// Do this in C++ for now
auto* globalObj = reinterpret_cast<GlobalObject*>(obj);
switch (operation) {
case JSPromiseRejectionOperation::Reject:
case JSPromiseRejectionOperation::Reject: {
if (obj->hasDebugger()) {
obj->debugger()->exception(obj, nullptr, promise->result(), false);
}
globalObj->m_aboutToBeNotifiedRejectedPromises.append(JSC::Strong<JSPromise>(obj->vm(), promise));
break;
}
case JSPromiseRejectionOperation::Handle:
globalObj->m_aboutToBeNotifiedRejectedPromises.removeFirstMatching([&](Strong<JSPromise>& unhandledPromise) {
return unhandledPromise.get() == promise;
@@ -3759,17 +3795,6 @@ void GlobalObject::installAPIGlobals(JSClassRef* globals, int count, JSC::VM& vm
extraStaticGlobals.releaseBuffer();
}
extern "C" bool JSC__JSGlobalObject__startRemoteInspector(JSC__JSGlobalObject* globalObject, unsigned char* host, uint16_t arg1)
{
#if !ENABLE(REMOTE_INSPECTOR)
return false;
#else
globalObject->setInspectable(true);
auto& server = Inspector::RemoteInspectorServer::singleton();
return server.start(reinterpret_cast<const char*>(host), arg1);
#endif
}
template<typename Visitor>
void GlobalObject::visitChildrenImpl(JSCell* cell, Visitor& visitor)
{

View File

@@ -403,6 +403,8 @@ public:
void* napiInstanceDataFinalizer = nullptr;
void* napiInstanceDataFinalizerHint = nullptr;
void* bunInspectorPtr = nullptr;
#include "ZigGeneratedClasses+lazyStructureHeader.h"
private:

View File

@@ -2772,8 +2772,10 @@ pub const JSGlobalObject = extern struct {
return cppFn("handleRejectedPromises", .{this});
}
extern fn JSGlobalObject__startRemoteInspector(*JSGlobalObject, [*:0]const u8, u16) bool;
pub fn startRemoteInspector(this: *JSGlobalObject, host: [:0]const u8, port: u16) bool {
return cppFn("startRemoteInspector", .{ this, host, port });
if (comptime is_bindgen) unreachable;
return JSGlobalObject__startRemoteInspector(this, host, port);
}
extern fn ZigGlobalObject__readableStreamToArrayBuffer(*JSGlobalObject, JSValue) JSValue;
@@ -2812,7 +2814,6 @@ pub const JSGlobalObject = extern struct {
"vm",
"generateHeapSnapshot",
"startRemoteInspector",
"handleRejectedPromises",
"createSyntheticModule_",
"queueMicrotaskJob",

View File

@@ -39,8 +39,14 @@ pub const ZigGlobalObject = extern struct {
pub const namespace = shim.namespace;
pub const Interface: type = NewGlobalObject(JS.VirtualMachine);
pub fn create(class_ref: [*]CAPI.JSClassRef, count: i32, console: *anyopaque) *JSGlobalObject {
var global = shim.cppFn("create", .{ class_ref, count, console });
pub const Inspect = enum(i32) {
none = 0,
port = 1,
breakpoint = 2,
};
pub fn create(class_ref: [*]CAPI.JSClassRef, count: i32, console: *anyopaque, inspect: Inspect) *JSGlobalObject {
var global = shim.cppFn("create", .{ class_ref, count, console, @enumToInt(inspect) });
Backtrace.reloadHandlers() catch unreachable;
return global;
}

View File

@@ -1,3 +1,4 @@
//-- AUTOGENERATED FILE -- 1681281993
// clang-format off
//-- GENERATED FILE. Do not edit --
//

View File

@@ -282,7 +282,6 @@ CPP_DECL void JSC__JSGlobalObject__handleRejectedPromises(JSC__JSGlobalObject* a
CPP_DECL JSC__JSValue JSC__JSGlobalObject__putCachedObject(JSC__JSGlobalObject* arg0, const ZigString* arg1, JSC__JSValue JSValue2);
CPP_DECL void JSC__JSGlobalObject__queueMicrotaskJob(JSC__JSGlobalObject* arg0, JSC__JSValue JSValue1, JSC__JSValue JSValue2, JSC__JSValue JSValue3, JSC__JSValue JSValue4);
CPP_DECL void JSC__JSGlobalObject__reload(JSC__JSGlobalObject* arg0);
CPP_DECL bool JSC__JSGlobalObject__startRemoteInspector(JSC__JSGlobalObject* arg0, unsigned char* arg1, uint16_t arg2);
CPP_DECL JSC__VM* JSC__JSGlobalObject__vm(JSC__JSGlobalObject* arg0);
#pragma mark - JSC::JSMap
@@ -567,7 +566,7 @@ ZIG_DECL JSC__JSValue Crypto__timingSafeEqual__slowpath(JSC__JSGlobalObject* arg
#pragma mark - Zig::GlobalObject
CPP_DECL JSC__JSGlobalObject* Zig__GlobalObject__create(JSClassRef* arg0, int32_t arg1, void* arg2);
CPP_DECL JSC__JSGlobalObject* Zig__GlobalObject__create(JSClassRef* arg0, int32_t arg1, void* arg2, int32_t Inspect3);
CPP_DECL void* Zig__GlobalObject__getModuleRegistryMap(JSC__JSGlobalObject* arg0);
CPP_DECL bool Zig__GlobalObject__resetModuleRegistryMap(JSC__JSGlobalObject* arg0, void* arg1);

View File

@@ -189,7 +189,6 @@ pub extern fn JSC__JSGlobalObject__handleRejectedPromises(arg0: *bindings.JSGlob
pub extern fn JSC__JSGlobalObject__putCachedObject(arg0: *bindings.JSGlobalObject, arg1: [*c]const ZigString, JSValue2: JSC__JSValue) JSC__JSValue;
pub extern fn JSC__JSGlobalObject__queueMicrotaskJob(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue, JSValue2: JSC__JSValue, JSValue3: JSC__JSValue, JSValue4: JSC__JSValue) void;
pub extern fn JSC__JSGlobalObject__reload(arg0: *bindings.JSGlobalObject) void;
pub extern fn JSC__JSGlobalObject__startRemoteInspector(arg0: *bindings.JSGlobalObject, arg1: [*c]u8, arg2: u16) bool;
pub extern fn JSC__JSGlobalObject__vm(arg0: *bindings.JSGlobalObject) *bindings.VM;
pub extern fn JSC__JSMap__create(arg0: *bindings.JSGlobalObject) JSC__JSValue;
pub extern fn JSC__JSMap__get_(arg0: ?*bindings.JSMap, arg1: *bindings.JSGlobalObject, JSValue2: JSC__JSValue) JSC__JSValue;
@@ -340,7 +339,7 @@ pub extern fn Reader__intptr__put(arg0: *bindings.JSGlobalObject, JSValue1: JSC_
pub extern fn Crypto__getRandomValues__put(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) void;
pub extern fn Crypto__randomUUID__put(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) void;
pub extern fn Crypto__timingSafeEqual__put(arg0: *bindings.JSGlobalObject, JSValue1: JSC__JSValue) void;
pub extern fn Zig__GlobalObject__create(arg0: [*c]JSClassRef, arg1: i32, arg2: ?*anyopaque) *bindings.JSGlobalObject;
pub extern fn Zig__GlobalObject__create(arg0: [*c]JSClassRef, arg1: i32, arg2: ?*anyopaque, Inspect3: i32) *bindings.JSGlobalObject;
pub extern fn Zig__GlobalObject__getModuleRegistryMap(arg0: *bindings.JSGlobalObject) ?*anyopaque;
pub extern fn Zig__GlobalObject__resetModuleRegistryMap(arg0: *bindings.JSGlobalObject, arg1: ?*anyopaque) bool;
pub extern fn Bun__Path__create(arg0: *bindings.JSGlobalObject, arg1: bool) JSC__JSValue;

BIN
src/bun.js/bun.lockb Executable file

Binary file not shown.

View File

@@ -879,3 +879,12 @@ pub const AnyEventLoop = union(enum) {
}
}
};
pub export fn Bun__tickWhileWaitingForDebugger(globalObject: *JSC.JSGlobalObject) callconv(.C) void {
globalObject.bunVM().eventLoop().tickPossiblyForever();
}
comptime {
if (!JSC.is_bindgen)
_ = Bun__tickWhileWaitingForDebugger;
}

View File

@@ -256,6 +256,28 @@ pub export fn Bun__drainMicrotasks() void {
JSC.VirtualMachine.get().eventLoop().tick();
}
const WaitForDebugger = struct {
var is_debugger_ready = std.atomic.Atomic(u32).init(0);
pub fn ready() void {
is_debugger_ready.store(1, .Monotonic);
std.Thread.Futex.wake(&is_debugger_ready, 1);
}
pub fn wait() void {
while (is_debugger_ready.load(.Monotonic) == 0) {
std.Thread.Futex.wait(&is_debugger_ready, 1);
}
}
};
pub export fn Bun__waitForDebuggerToStart() void {
WaitForDebugger.wait();
}
pub export fn Bun__debuggerIsReady() void {
WaitForDebugger.ready();
}
export fn Bun__readOriginTimer(vm: *JSC.VirtualMachine) u64 {
return vm.origin_timer.read();
}
@@ -320,6 +342,12 @@ pub export fn Bun__handleRejectedPromise(global: *JSGlobalObject, promise: *JSC.
jsc_vm.autoGarbageCollect();
}
pub export fn Bun__getMainPath(globalObject: *JSC.JSGlobalObject, out: *ZigString) void {
var jsc_vm = globalObject.bunVM();
out.* = ZigString.fromUTF8(jsc_vm.main);
}
pub export fn Bun__onDidAppendPlugin(jsc_vm: *VirtualMachine, globalObject: *JSGlobalObject) void {
if (jsc_vm.plugin_runner != null) {
return;
@@ -449,6 +477,8 @@ pub const VirtualMachine = struct {
gc_controller: JSC.GarbageCollectionController = .{},
auto_inspect: bool = false,
pub const OnUnhandledRejection = fn (*VirtualMachine, globalObject: *JSC.JSGlobalObject, JSC.JSValue) void;
pub const OnException = fn (*ZigException) void;
@@ -812,6 +842,7 @@ pub const VirtualMachine = struct {
&global_classes,
@intCast(i32, global_classes.len),
vm.console,
.none,
);
vm.regular_event_loop.global = vm.global;
vm.regular_event_loop.virtual_machine = vm;
@@ -826,16 +857,21 @@ pub const VirtualMachine = struct {
return vm;
}
pub const VMOptions = struct {
env_loader: ?*DotEnv.Loader = null,
log: ?*logger.Log = null,
existing_bundle: ?*NodeModuleBundle = null,
store_fd: bool = false,
inspector: JSC.ZigGlobalObject.Inspect = .none,
};
pub fn init(
allocator: std.mem.Allocator,
_args: Api.TransformOptions,
existing_bundle: ?*NodeModuleBundle,
_log: ?*logger.Log,
env_loader: ?*DotEnv.Loader,
store_fd: bool,
vm_opts: VMOptions,
) !*VirtualMachine {
var log: *logger.Log = undefined;
if (_log) |__log| {
if (vm_opts.log) |__log| {
log = __log;
} else {
log = try allocator.create(logger.Log);
@@ -849,8 +885,8 @@ pub const VirtualMachine = struct {
allocator,
log,
try Config.configureTransformOptionsForBunVM(allocator, _args),
existing_bundle,
env_loader,
vm_opts.existing_bundle,
vm_opts.env_loader,
);
var vm = VMHolder.vm.?;
@@ -884,7 +920,7 @@ pub const VirtualMachine = struct {
vm.event_loop = &vm.regular_event_loop;
vm.bundler.macro_context = null;
vm.bundler.resolver.store_fd = store_fd;
vm.bundler.resolver.store_fd = vm_opts.store_fd;
vm.bundler.resolver.onWakePackageManager = .{
.context = &vm.modules,
@@ -896,7 +932,7 @@ pub const VirtualMachine = struct {
try vm.bundler.configureFramework(false);
vm.bundler.macro_context = js_ast.Macro.MacroContext.init(&vm.bundler);
vm.auto_inspect = vm_opts.inspector != .none;
if (_args.serve orelse false) {
vm.bundler.linker.onImportCSS = Bun.onImportCSS;
}
@@ -909,6 +945,7 @@ pub const VirtualMachine = struct {
&global_classes,
@intCast(i32, global_classes.len),
vm.console,
vm_opts.inspector,
);
vm.regular_event_loop.global = vm.global;
vm.regular_event_loop.virtual_machine = vm;
@@ -1672,6 +1709,11 @@ pub const VirtualMachine = struct {
JSC.JSValue.fromCell(promise).ensureStillAlive();
}
if (this.auto_inspect) {
this.auto_inspect = false;
_ = this.global.startRemoteInspector("0.0.0.0", 9229);
}
return promise;
}
@@ -2771,3 +2813,11 @@ pub fn NewHotReloader(comptime Ctx: type, comptime EventLoopType: type, comptime
}
};
}
comptime {
if (!JSC.is_bindgen) {
_ = Bun__getMainPath;
_ = Bun__waitForDebuggerToStart;
_ = Bun__debuggerIsReady;
}
}

6
src/bun.js/package.json Normal file
View File

@@ -0,0 +1,6 @@
{
"private": true,
"devDependencies": {
"expect": "^29.5.0"
}
}

View File

@@ -59,7 +59,7 @@ pub const Run = struct {
try bun.CLI.Arguments.loadConfigPath(ctx.allocator, true, "bunfig.toml", &ctx, .RunCommand);
}
run = .{
run = Run{
.vm = try VirtualMachine.initWithModuleGraph(arena.allocator(), ctx.log, graph_ptr),
.arena = arena,
.ctx = ctx,
@@ -147,10 +147,12 @@ pub const Run = struct {
.vm = try VirtualMachine.init(
arena.allocator(),
ctx.args,
null,
ctx.log,
null,
ctx.debug.hot_reload != .none,
.{
.log = ctx.log,
.store_fd = ctx.debug.hot_reload != .none,
.inspector = ctx.debug.inspector,
.env_loader = null,
},
),
.arena = arena,
.ctx = ctx,

View File

@@ -173,6 +173,8 @@ pub const Arguments = struct {
clap.parseParam("--prefer-offline Skip staleness checks for packages in bun's JavaScript runtime and resolve from disk") catch unreachable,
clap.parseParam("--prefer-latest Use the latest matching versions of packages in bun's JavaScript runtime, always checking npm") catch unreachable,
clap.parseParam("--silent Don't repeat the command for bun run") catch unreachable,
clap.parseParam("--inspect-brk Start a debugger with a breakpoint set") catch unreachable,
clap.parseParam("--inspect Start a debugger") catch unreachable,
};
const public_params = shared_public_params ++ not_bun_dev_flags;
@@ -185,8 +187,8 @@ pub const Arguments = struct {
pub const dev_params = [_]ParamType{
clap.parseParam("--disable-bun.js Disable bun.js from loading in the dev server") catch unreachable,
clap.parseParam("--disable-react-fast-refresh Disable React Fast Refresh") catch unreachable,
clap.parseParam("--bunfile <STR> Use a .bun file (default: node_modules.bun)") catch unreachable,
clap.parseParam("--server-bunfile <STR> Use a .server.bun file (default: node_modules.server.bun)") catch unreachable,
clap.parseParam("--bunfile <STR> Use a .bun file (DEPRECATED)") catch unreachable,
clap.parseParam("--server-bunfile <STR> Use a .server.bun file (DEPRECATED)") catch unreachable,
clap.parseParam("--public-dir <STR> Top-level directory for .html files, fonts or anything external. Defaults to \"<cwd>/public\", to match create-react-app and Next.js") catch unreachable,
clap.parseParam("--disable-hmr Disable Hot Module Reloading (disables fast refresh too) in bun dev") catch unreachable,
clap.parseParam("--use <STR> Choose a framework, e.g. \"--use next\". It checks first for a package named \"bun-framework-packagename\" and then \"packagename\".") catch unreachable,
@@ -471,8 +473,7 @@ pub const Arguments = struct {
const print_help = args.flag("--help");
if (print_help) {
const params_len = if (cmd == .BuildCommand) build_params_public.len else public_params.len;
clap.help(Output.writer(), params_to_use[0..params_len]) catch {};
clap.help(Output.writer(), params_to_use) catch {};
Output.prettyln("\n-------\n\n", .{});
Output.flush();
HelpCommand.printWithReason(.explicit);
@@ -686,6 +687,12 @@ pub const Arguments = struct {
// const ResolveMatcher = strings.ExactSizeMatcher(8);
opts.resolve = Api.ResolveMode.lazy;
if (comptime cmd == .RunCommand or cmd == .AutoCommand or cmd == .TestCommand) {
if (args.flag("--inspect-brk"))
ctx.debug.inspector = .breakpoint
else if (args.flag("--inspect"))
ctx.debug.inspector = .port;
}
const TargetMatcher = strings.ExactSizeMatcher(8);
@@ -906,6 +913,8 @@ pub const Command = struct {
run_in_bun: bool = false,
loaded_bunfig: bool = false,
inspector: bun.JSC.ZigGlobalObject.Inspect = .none,
// technical debt
macros: MacroOptions = MacroOptions.unspecified,
editor: string = "",

View File

@@ -465,15 +465,19 @@ pub const TestCommand = struct {
var vm = try JSC.VirtualMachine.init(
ctx.allocator,
ctx.args,
null,
ctx.log,
env_loader,
// we must store file descriptors because we reuse them for
// iterating through the directory tree recursively
//
// in the future we should investigate if refactoring this to not
// rely on the dir fd yields a performance improvement
true,
.{
.log = ctx.log,
.env_loader = env_loader,
.existing_bundle = null,
.store_fd =
// we must store file descriptors because we reuse them for
// iterating through the directory tree recursively
//
// in the future we should investigate if refactoring this to not
// rely on the dir fd yields a performance improvement
true,
.inspector = ctx.debug.inspector,
},
);
vm.argv = ctx.passthrough;
vm.preload = ctx.preloads;

View File

@@ -1467,10 +1467,12 @@ pub const RequestContext = struct {
var vm: *JavaScript.VirtualMachine = JavaScript.VirtualMachine.init(
bun.default_allocator,
handler.args,
null,
handler.log,
handler.env_loader,
true,
.{
.log = handler.log,
.env_loader = handler.env_loader,
.store_fd = true,
.inspector = .none,
},
) catch |err| {
handler.handleJSError(.create_vm, err) catch {};
javascript_disabled = true;

View File

@@ -9595,14 +9595,13 @@ pub const Macro = struct {
resolver.opts.transform_options.node_modules_bundle_path = null;
resolver.opts.transform_options.node_modules_bundle_path_server = null;
defer resolver.opts.transform_options = old_transform_options;
var _vm = try JavaScript.VirtualMachine.init(
default_allocator,
resolver.opts.transform_options,
null,
log,
env,
false,
);
var _vm = try JavaScript.VirtualMachine.init(default_allocator, resolver.opts.transform_options, .{
.log = log,
.existing_bundle = null,
.store_fd = false,
.inspector = .none,
.env_loader = env,
});
_vm.enableMacroMode();
_vm.eventLoop().ensureWaker();