Skip to content

Commit 34bf90e

Browse files
Use CDP (if available) to inject instrumentation to avoid conflicts (#63)
1 parent a02df88 commit 34bf90e

File tree

6 files changed

+218
-69
lines changed

6 files changed

+218
-69
lines changed

.changeset/sharp-sides-relax.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"effect-vscode": minor
3+
---
4+
5+
Use CDP (if available) to inject instrumentation to avoid scope conflicts

src/DebugChannel.ts

Lines changed: 119 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
1+
import * as ChannelSchema from "@effect/platform/ChannelSchema"
2+
import * as Socket from "@effect/platform/Socket"
13
import * as Array from "effect/Array"
24
import * as Data from "effect/Data"
35
import * as Effect from "effect/Effect"
6+
import * as Function from "effect/Function"
7+
import * as Mailbox from "effect/Mailbox"
48
import * as Option from "effect/Option"
59
import type * as ParseResult from "effect/ParseResult"
10+
import * as Predicate from "effect/Predicate"
611
import * as Schema from "effect/Schema"
712
import * as SchemaAST from "effect/SchemaAST"
13+
import * as Stream from "effect/Stream"
14+
import * as ws from "ws"
815
import * as VsCode from "./VsCode"
916

10-
export class DebugChannelError extends Data.TaggedError("VariableExtractError")<{
17+
export class DebugChannelError extends Data.TaggedError("DebugChannelError")<{
1118
readonly message: string
1219
}> {}
1320

@@ -31,8 +38,44 @@ export class DebugChannel extends Effect.Tag("effect-vscode/DebugChannel")<Debug
3138
threadId: number | undefined
3239
}
3340
) => Effect.Effect<VariableReference, DebugChannelError, never>
41+
evaluateOnEveryExecutionContext: (
42+
opts: {
43+
expression: string
44+
}
45+
) => Effect.Effect<void, DebugChannelError, never>
3446
}>() {}
3547

48+
export interface CdpProxyClient {
49+
readonly queue: Mailbox.ReadonlyMailbox<unknown>
50+
readonly request: (_: unknown) => Effect.Effect<void>
51+
}
52+
53+
export const run = Effect.fnUntraced(
54+
function*<R, E, _>(socket: Socket.Socket, handle: (client: CdpProxyClient) => Effect.Effect<_, E, R>) {
55+
const responses = yield* Mailbox.make<unknown>()
56+
const requests = yield* Mailbox.make<unknown>()
57+
58+
const client: CdpProxyClient = {
59+
queue: requests,
60+
request: (res) => responses.offer(res)
61+
}
62+
63+
yield* Mailbox.toStream(responses).pipe(
64+
Stream.pipeThroughChannel(
65+
ChannelSchema.duplexUnknown(Socket.toChannelString(socket), {
66+
inputSchema: Schema.parseJson(Schema.Unknown),
67+
outputSchema: Schema.parseJson(Schema.Unknown)
68+
})
69+
),
70+
Stream.runForEach((req) => requests.offer(req)),
71+
Effect.ensuring(Effect.zipRight(responses.shutdown, requests.shutdown)),
72+
Effect.forkScoped
73+
)
74+
75+
yield* handle(client)
76+
}
77+
)
78+
3679
interface DapVariableReference {
3780
readonly name: string
3881
readonly value: string
@@ -142,7 +185,12 @@ export const makeVsCodeDebugSession = (debugSession: VsCode.VsCodeDebugSession["
142185
})
143186
}
144187
const elementAst = index < ast.elements.length ? ast.elements[index] : ast.rest[0]
145-
return extractValue(indexProperty, elementAst.type)
188+
return extractValue(indexProperty, elementAst.type).pipe(
189+
Effect.catchTag(
190+
"DebugChannelError",
191+
(e) => new DebugChannelError({ message: "at index " + index + " " + e.message })
192+
)
193+
)
146194
})
147195
return yield* Effect.all(elements, { concurrency: "unbounded" })
148196
}
@@ -173,6 +221,14 @@ export const makeVsCodeDebugSession = (debugSession: VsCode.VsCodeDebugSession["
173221
result[propertySignature.name] = extractValue(
174222
propertyVariableReference,
175223
propertySignature.type
224+
).pipe(
225+
Effect.catchTag(
226+
"DebugChannelError",
227+
(e) =>
228+
new DebugChannelError({
229+
message: "in property " + String(propertySignature.name) + " " + e.message
230+
})
231+
)
176232
)
177233
}
178234
return yield* Effect.all(result, { concurrency: "unbounded" })
@@ -207,6 +263,66 @@ export const makeVsCodeDebugSession = (debugSession: VsCode.VsCodeDebugSession["
207263
}
208264

209265
return DebugChannel.of({
266+
evaluateOnEveryExecutionContext: (_opts) =>
267+
Effect.gen(function*() {
268+
const addr = yield* VsCode.executeCommand<{ host: string; port: number; path?: string }>(
269+
"extension.js-debug.requestCDPProxy",
270+
debugSession.id
271+
)
272+
const uri = `ws://${addr.host}:${addr.port}${addr.path || ""}`
273+
274+
const cdpSocket = yield* Socket.fromWebSocket(
275+
Effect.sync(() => {
276+
const wss = new ws.WebSocket(uri, {
277+
perMessageDeflate: false,
278+
maxPayload: 256 * 1024 * 1024
279+
})
280+
281+
return wss as any
282+
})
283+
)
284+
285+
yield* run(
286+
cdpSocket,
287+
Effect.fn(function*(client) {
288+
// we enable reporting of execution contexts
289+
yield* client.request({
290+
method: "Runtime.enable"
291+
})
292+
293+
while (true) {
294+
const response = yield* client.queue.take
295+
if (
296+
Predicate.hasProperty(response, "method") && response.method === "Runtime.executionContextCreated" &&
297+
Predicate.hasProperty(response, "params") && Predicate.hasProperty(response.params, "context") &&
298+
Predicate.hasProperty(response.params.context, "id")
299+
) {
300+
const contextId = response.params.context.id
301+
302+
// we send as notification and silent because we don't want to pollute the output
303+
yield* client.request({
304+
method: "Runtime.evaluate",
305+
params: {
306+
expression: _opts.expression,
307+
contextId,
308+
silent: true
309+
}
310+
})
311+
}
312+
}
313+
})
314+
)
315+
}).pipe(
316+
Effect.scoped,
317+
Effect.timeoutTo({
318+
onTimeout: Function.constUndefined,
319+
onSuccess: Function.identity,
320+
duration: 1000
321+
}),
322+
Effect.catchAll((socketError) => {
323+
return new DebugChannelError({ message: String(socketError) })
324+
})
325+
),
210326
evaluate: (opts) =>
211327
Effect.gen(function*() {
212328
let request: any = {
@@ -224,7 +340,7 @@ export const makeVsCodeDebugSession = (debugSession: VsCode.VsCodeDebugSession["
224340
const stackTraces = yield* debugRequest<DapStackTracesResponse>("stackTrace", {
225341
threadId
226342
})
227-
const stackTrace = stackTraces.stackFrames[stackTraces.stackFrames.length - 1]
343+
const stackTrace = stackTraces.stackFrames[0]
228344
if (stackTrace) {
229345
request = {
230346
expression: opts.expression,

src/DebugEnv.ts

Lines changed: 73 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,29 @@ export class DebugEnv extends Context.Tag("effect-vscode/DebugEnv")<
9999

100100
// --
101101

102-
export const ensureInstrumentationInjected = (guessFrameId: boolean, threadId?: number) =>
102+
export const ensureInstrumentationInjected = (
103+
guessFrameId: boolean,
104+
threadId?: number
105+
) =>
103106
Effect.gen(function*() {
104107
const result = yield* DebugChannel.DebugChannel.evaluate({
105-
expression: `globalThis && "effect/devtools/instrumentation" in globalThis`,
108+
expression: `(globalThis && "effect/devtools/instrumentation" in globalThis)`,
106109
guessFrameId,
107110
threadId
108111
})
109112
const isInjected = yield* result.parse(Schema.Boolean)
110113
if (!isInjected) {
111-
yield* DebugChannel.DebugChannel.evaluate({ expression: compiledInstrumentationString, guessFrameId, threadId })
114+
yield* DebugChannel.DebugChannel.evaluateOnEveryExecutionContext({
115+
expression: compiledInstrumentationString
116+
}).pipe(
117+
Effect.orElse(() =>
118+
DebugChannel.DebugChannel.evaluate({
119+
expression: compiledInstrumentationString,
120+
guessFrameId,
121+
threadId
122+
})
123+
)
124+
)
112125
}
113126
})
114127

@@ -169,75 +182,83 @@ const SpanSchema = Schema.Struct({
169182
})
170183

171184
const ExternalSpanSchema = Schema.Struct({
172-
_tag: Schema.Literal("External"),
185+
_tag: Schema.Literal("ExternalSpan"),
173186
spanId: Schema.String,
174187
traceId: Schema.String
175188
})
176189

177-
const SpanStackSchema = Schema.Array(Schema.Union(SpanSchema, ExternalSpanSchema))
190+
const AnySpanSchema = Schema.Union(SpanSchema, ExternalSpanSchema)
191+
export type AnySpanSchema = Schema.Schema.Type<typeof AnySpanSchema>
192+
193+
function spanEntryToSpanStackEntry(entry: AnySpanSchema | undefined): Array<SpanStackEntry> {
194+
const spans: Array<SpanStackEntry> = []
195+
if (!entry) return []
196+
switch (entry._tag) {
197+
case "Span": {
198+
let match = false
199+
for (let stackIndex = 0; stackIndex < entry.stack.length; stackIndex++) {
200+
const stackLine = entry.stack[stackIndex]!
201+
match = true
202+
spans.push(
203+
new SpanStackEntry({
204+
...stackLine,
205+
...entry,
206+
stackIndex
207+
})
208+
)
209+
}
210+
211+
if (!match) {
212+
spans.push(new SpanStackEntry({ ...entry, stackIndex: -1, line: 0, column: 0 }))
213+
}
214+
break
215+
}
216+
case "ExternalSpan": {
217+
spans.push(
218+
new SpanStackEntry({
219+
...entry,
220+
name: "<external span " + entry.spanId + ">",
221+
stackIndex: -1,
222+
line: 0,
223+
column: 0,
224+
attributes: []
225+
})
226+
)
227+
break
228+
}
229+
}
230+
return spans
231+
}
232+
233+
const FiberCurrentSpanResponseSchema = Schema.Array(Schema.Union(SpanSchema, ExternalSpanSchema))
178234

179-
function getFiberCurrentSpan(currentFiberExpression: string, threadId: number | undefined) {
235+
function getFiberCurrentSpan(currentFiberExpression: string, maxDepth: number, threadId: number | undefined) {
180236
return Effect.gen(function*() {
181237
yield* ensureInstrumentationInjected(true, threadId)
182238
const result = yield* DebugChannel.DebugChannel.evaluate({
183-
expression: `globalThis["effect/devtools/instrumentation"].getFiberCurrentSpanStack(${currentFiberExpression})`,
239+
expression:
240+
`globalThis["effect/devtools/instrumentation"].getFiberCurrentSpanStack(${currentFiberExpression}, ${maxDepth})`,
184241
guessFrameId: true,
185242
threadId
186243
})
187-
return yield* result.parse(SpanStackSchema)
244+
return yield* result.parse(FiberCurrentSpanResponseSchema)
188245
}).pipe(
189246
Effect.tapError(Effect.logError),
190247
Effect.orElseSucceed(() => []),
191248
Effect.map((stack) => {
192249
// now, a single span can have a stack with multiple locations
193250
// so we need to duplicate the span for each location
194-
const spans: Array<SpanStackEntry> = []
195-
const stackEntries = [...stack]
196-
while (stackEntries.length > 0) {
197-
const entry = stackEntries.shift()!
198-
switch (entry._tag) {
199-
case "Span": {
200-
let match = false
201-
for (let stackIndex = 0; stackIndex < entry.stack.length; stackIndex++) {
202-
const stackLine = entry.stack[stackIndex]!
203-
match = true
204-
spans.push(
205-
new SpanStackEntry({
206-
...stackLine,
207-
...entry,
208-
stackIndex
209-
})
210-
)
211-
}
212-
213-
if (!match) {
214-
spans.push(new SpanStackEntry({ ...entry, stackIndex: -1, line: 0, column: 0 }))
215-
}
216-
break
217-
}
218-
case "External": {
219-
spans.push(
220-
new SpanStackEntry({
221-
...entry,
222-
name: "<external span " + entry.spanId + ">",
223-
stackIndex: -1,
224-
line: 0,
225-
column: 0,
226-
attributes: []
227-
})
228-
)
229-
break
230-
}
231-
}
251+
let spans: Array<SpanStackEntry> = []
252+
for (const entry of stack) {
253+
spans = [...spans, ...spanEntryToSpanStackEntry(entry)]
232254
}
233-
234255
return spans
235256
})
236257
)
237258
}
238259

239260
export const getCurrentSpanStack = (threadId: number | undefined) =>
240-
getFiberCurrentSpan(`globalThis["effect/FiberCurrent"]`, threadId)
261+
getFiberCurrentSpan(`globalThis["effect/FiberCurrent"]`, 0, threadId)
241262

242263
// --
243264

@@ -271,9 +292,11 @@ const getCurrentFibers = (threadId: number | undefined) =>
271292
Effect.all(
272293
fibers.map((fiber, idx) =>
273294
Effect.map(
274-
getFiberCurrentSpan(`(globalThis["effect/devtools/instrumentation"].fibers || [])[${idx}]`, threadId),
295+
getFiberCurrentSpan(`(globalThis["effect/devtools/instrumentation"].fibers || [])[${idx}]`, 1, threadId),
275296
(stack) => new FiberEntry({ ...fiber, stack })
276-
), { concurrency: "unbounded" })
297+
)
298+
),
299+
{ concurrency: "unbounded" }
277300
)
278301
)
279302
)

src/VsCode.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ export const dismissable = <A>(
3232
f: () => Thenable<A | undefined>
3333
): Effect.Effect<A, Cause.NoSuchElementException> => thenable(f).pipe(Effect.flatMap(Effect.fromNullable))
3434

35-
export const executeCommand = (command: string, ...args: Array<any>) =>
36-
thenable(() => vscode.commands.executeCommand(command, ...args))
35+
export const executeCommand = <A = unknown>(command: string, ...args: Array<any>) =>
36+
thenable(() => vscode.commands.executeCommand<A>(command, ...args))
3737

3838
export const registerCommand = <R, E, A>(
3939
command: string,

0 commit comments

Comments
 (0)