Skip to content

Commit f82bfc4

Browse files
feat(Wind): Implement MessageChannel for extension host IPC
Add proper MessageChannel implementation in Preload and Install.ts to handle VS Code's extension host protocol. The acquire() method now creates a MessageChannel pair, posts port2 to window for acquirePort() to pick up, and implements a minimal handshake (Ready/Initialized) to prevent VS Code from hanging for 60s. Also add stub for utilityProcessWorker.createWorker returning a never-resolving Promise to prevent destructure crashes in watcherClient, and implement readFileStream listener to stream file data using VSBuffer. Add IPC logging for stub channels. These changes enable the extension host to initialize properly while awaiting full Cocoon sidecar integration.
1 parent 04f0374 commit f82bfc4

6 files changed

Lines changed: 225 additions & 26 deletions

File tree

Source/Function/DevLog.ts

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,45 @@
1616
* localStorage.setItem("LAND_DEV_LOG", "config,folder");
1717
* ```
1818
*
19-
* ## Tags
20-
* - `vfs` — file stat, read, write, readdir
21-
* - `ipc` — TauriMainProcessService channel routing
22-
* - `config` — ResolveConfiguration, environment paths
23-
* - `lifecycle` — preload, polyfills, workbench loading
24-
* - `storage` — storage getItems/updateItems
25-
* - `exthost` — extension host starter
26-
* - `folder` — folder picker, workspace navigation
27-
* - `bootstrap` — Effect-TS bootstrap stages
19+
* ## Tags — Mountain (Rust) + Wind/Sky (TypeScript)
20+
*
21+
* | Tag | Scope |
22+
* |---------------|-----------------------------------------------------|
23+
* | `vfs` | File stat, read, write, readdir, mkdir, delete, copy|
24+
* | `ipc` | IPC routing: invoke dispatch, channel calls |
25+
* | `config` | Configuration get/set, env paths, workbench config |
26+
* | `lifecycle` | Startup, shutdown, phases, window events |
27+
* | `storage` | Storage get/set/delete, items, optimize |
28+
* | `folder` | Folder picker, workspace navigation |
29+
* | `exthost` | Extension host: create, start, kill, exit info |
30+
* | `extensions` | Extension scanning, activation, management |
31+
* | `terminal` | Terminal/PTY: create, sendText, profiles, shell |
32+
* | `search` | Search: findFiles, findInFiles |
33+
* | `themes` | Theme: list, get active, set |
34+
* | `window` | Window: focus, maximize, minimize, fullscreen |
35+
* | `nativehost` | OS integration: process, devtools, shell |
36+
* | `clipboard` | Clipboard: read/write text, buffer, image |
37+
* | `commands` | Command registry: execute, getAll |
38+
* | `model` | Text model: open, close, get, updateContent |
39+
* | `output` | Output channels: create, append, show |
40+
* | `notification`| Notifications: show, progress |
41+
* | `progress` | Progress: begin, end, report |
42+
* | `quickinput` | Quick input: showQuickPick, showInputBox |
43+
* | `workingcopy` | Working copy: dirty state |
44+
* | `workspaces` | Workspace: folders, recent, enter |
45+
* | `keybinding` | Keybindings: add, remove, lookup |
46+
* | `label` | Label service: getBase, getUri |
47+
* | `history` | Navigation history: push, goBack, goForward |
48+
* | `decorations` | Decorations: get, set, clear |
49+
* | `textfile` | Text file operations: read, write, save |
50+
* | `update` | Update service: check, download, apply |
51+
* | `encryption` | Encryption: encrypt, decrypt |
52+
* | `menubar` | Menubar updates |
53+
* | `url` | URL handler: registerExternalUriOpener |
54+
* | `grpc` | gRPC/Vine: server, client, connections |
55+
* | `cocoon` | Cocoon sidecar: spawn, health, handshake |
56+
* | `bootstrap` | Effect-TS bootstrap stages |
57+
* | `preload` | Preload: globals, polyfills, ipcRenderer |
2858
*/
2959

3060
let CachedTags: string[] | null = null;

Source/Function/Install/Function/Install.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,38 @@ export default async function Install(): Promise<void> {
7777
},
7878
webFrame: { setZoomLevel: () => {} },
7979
webUtils: { getPathForFile: (file: File) => file.name },
80-
ipcMessagePort: { acquire: () => {} },
80+
ipcMessagePort: {
81+
acquire: (ResponseChannel: string, Nonce: string) => {
82+
console.log(
83+
`[Wind] MessagePort acquire: ${ResponseChannel}, nonce=${Nonce}`,
84+
);
85+
const { port1, port2 } = new MessageChannel();
86+
window.postMessage(Nonce, "*", [port2]);
87+
port1.start();
88+
let Done = false;
89+
port1.onmessage = (Event: MessageEvent) => {
90+
if (Done) return;
91+
const Data = Event.data;
92+
const Length =
93+
Data instanceof ArrayBuffer
94+
? Data.byteLength
95+
: Data instanceof Uint8Array
96+
? Data.byteLength
97+
: 0;
98+
if (Length > 1) {
99+
Done = true;
100+
console.log(
101+
"[Wind] Extension host: received init data, sending Initialized",
102+
);
103+
port1.postMessage(new Uint8Array([1]));
104+
}
105+
};
106+
setTimeout(() => {
107+
console.log("[Wind] Extension host: sending Ready");
108+
port1.postMessage(new Uint8Array([2]));
109+
}, 50);
110+
},
111+
},
81112
};
82113

83114
// Attach to window

Source/Preload.ts

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -186,16 +186,62 @@ const ipcRenderer = {
186186

187187
const ipcMessagePort = {
188188
acquire: (responseChannel: string, nonce: string) => {
189-
// FUTURE: Implement proper MessageChannel for VSCode SharedProcessWorker
190189
console.log(
191-
`[Preload] MessagePort acquire requested: ${responseChannel}, ${nonce}`,
190+
`[Preload] MessagePort acquire: ${responseChannel}, nonce=${nonce}`,
192191
);
193192

194-
// For now, signal that ports are not available
195-
// This will need proper implementation for full VSCode compatibility
193+
// Create an in-memory MessageChannel.
194+
// port2 is posted to the window so acquirePort() (ipc.mp.ts) picks it up
195+
// via its window 'message' listener (filters e.data === nonce && e.ports[0]).
196+
// port1 implements a minimal extension host handshake so VS Code
197+
// does not hang for 60 s waiting for Ready / Initialized.
198+
const { port1, port2 } = new MessageChannel();
199+
200+
// acquirePort() filters: e.data === nonce && e.source === window
201+
window.postMessage(nonce, "*", [port2]);
202+
203+
port1.start();
204+
205+
let HandshakeComplete = false;
206+
207+
port1.onmessage = (Event: MessageEvent) => {
208+
if (HandshakeComplete) {
209+
// After handshake, silently drop extension-host protocol
210+
// messages (activate, executeCommand, etc.) until
211+
// a real Cocoon relay is wired.
212+
return;
213+
}
214+
215+
// The first large message from VS Code is the init data
216+
// (JSON-encoded IExtensionHostInitData wrapped in VSBuffer).
217+
// Any message with byteLength > 1 is init data; single-byte
218+
// messages are control (Ready=2, Initialized=1, Terminate=3).
219+
const Data = Event.data;
220+
const Length =
221+
Data instanceof ArrayBuffer
222+
? Data.byteLength
223+
: Data instanceof Uint8Array
224+
? Data.byteLength
225+
: typeof Data === "object" && Data?.byteLength
226+
? Data.byteLength
227+
: 0;
228+
229+
if (Length > 1) {
230+
HandshakeComplete = true;
231+
console.log(
232+
"[Preload] Extension host: received init data, sending Initialized",
233+
);
234+
// MessageType.Initialized → byte 1
235+
port1.postMessage(new Uint8Array([1]));
236+
}
237+
};
238+
239+
// Send Ready after a tick so VS Code's onMessage listener is registered.
240+
// MessageType.Ready → byte 2
196241
setTimeout(() => {
197-
ipcRenderer.send(responseChannel, nonce);
198-
}, 0);
242+
console.log("[Preload] Extension host: sending Ready");
243+
port1.postMessage(new Uint8Array([2]));
244+
}, 50);
199245
},
200246
};
201247

Source/Service/TauriMainProcessService.ts

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,14 @@ const StubChannels: Record<string, Record<string, unknown>> = {
135135
},
136136
},
137137
sharedProcess: {},
138-
utilityProcessWorker: { createWorker: undefined },
138+
utilityProcessWorker: {
139+
// createWorker returns a never-resolving promise to prevent
140+
// destructure crash in watcherClient/utilityProcessWorkerWorkbenchService.
141+
// Without a real utility process, the watcher and other workers
142+
// simply never initialize (non-blocking — Mountain handles file watching).
143+
createWorker: new Promise(() => {}),
144+
disposeWorker: undefined,
145+
},
139146
// Channels with no desktop equivalent in Tauri
140147
meteredConnection: {},
141148
webContentExtractor: {},
@@ -203,6 +210,7 @@ class TauriChannel implements IChannel {
203210
// Stub channels (not yet wired to Mountain)
204211
const Stubs = StubChannels[this.ChannelName];
205212
if (Stubs !== undefined) {
213+
DevLog("ipc", `stub: ${this.ChannelName}.${Command}`);
206214
const StubValue = Stubs[Command];
207215
if (StubValue !== undefined) {
208216
return StubValue as T;
@@ -297,10 +305,50 @@ class TauriChannel implements IChannel {
297305
return undefined as T;
298306
}
299307

300-
listen<T>(_Event: string, _Arg?: unknown): VSCodeEvent<T> {
301-
// Event subscriptions — return a no-op event for now.
302-
// These should be wired to Tauri event listeners (AppHandle.emit)
303-
// when Mountain emits sky:// events.
308+
listen<T>(Event: string, Arg?: unknown): VSCodeEvent<T> {
309+
DevLog("ipc", `listen: ${this.ChannelName}.${Event}`);
310+
311+
// readFileStream: import real VSBuffer from VS Code's buffer.js
312+
// (relative path resolves from /Static/Application/vs/platform/ipc/electron-browser/).
313+
if (
314+
FileSystemChannels.has(this.ChannelName) &&
315+
Event === "readFileStream"
316+
) {
317+
return ((Listener: (DataOrErrorOrEnd: unknown) => void) => {
318+
const Params =
319+
Arg !== undefined ? (Array.isArray(Arg) ? Arg : [Arg]) : [];
320+
321+
Promise.all([
322+
import("../../../base/common/buffer.js") as Promise<{
323+
VSBuffer: { wrap(buffer: Uint8Array): unknown };
324+
}>,
325+
InvokeMountain(`${this.RoutePrefix}:readFile`, Params),
326+
])
327+
.then(([{ VSBuffer }, Result]) => {
328+
const Raw = Result as
329+
| { buffer: number[] }
330+
| number[]
331+
| null
332+
| undefined;
333+
if (Raw !== null && Raw !== undefined) {
334+
const Arr = Array.isArray(Raw)
335+
? Raw
336+
: (Raw as { buffer: number[] }).buffer;
337+
if (Array.isArray(Arr)) {
338+
Listener(VSBuffer.wrap(new Uint8Array(Arr)));
339+
}
340+
}
341+
Listener("end" as unknown);
342+
})
343+
.catch((Err) => {
344+
Listener(Err);
345+
});
346+
347+
return { dispose: () => {} };
348+
}) as unknown as VSCodeEvent<T>;
349+
}
350+
351+
// Other events — return no-op for now.
304352
return (() => ({ dispose: () => {} })) as unknown as VSCodeEvent<T>;
305353
}
306354
}

Target/Function/Install/Function/Install.js

Lines changed: 27 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Target/Preload.js

Lines changed: 22 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)