Skip to content

Commit cdf3743

Browse files
feat(Wind): Add development logging and real path resolution from Mountain
Add DevLog utility for tag-filtered browser-side logging controlled via window.__LAND_DEV_LOG or localStorage. Supports tags: vfs, ipc, config, lifecycle, storage, exthost, folder, bootstrap. ResolveConfiguration now fetches real paths from Mountain via MountainIPCInvoke (nativeHost:getEnvironmentPaths) instead of hardcoded values. This provides homeDir, tmpDir, userDataDir, logsPath for the sandbox config. Also reads ?folder= URL parameter to set workspace/folderUri for folder-based launches. Add IPC request/response logging in TauriMainProcessService and fix file read commands to return proper Uint8Array format for VS Code's IPCFileSystemProvider. Also improve error handling in ChildProcessPolyfill and SharedProcessProxy with catch handlers for Tauri event listeners. These changes enable the workbench to initialize with real system paths and add development debugging capabilities for IPC, configuration, and file operations.
1 parent e6f33b2 commit cdf3743

11 files changed

Lines changed: 342 additions & 54 deletions

File tree

Source/Effect/Bootstrap/index.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,16 @@ export { BootstrapLive } from "./Implementation/BootstrapImplementation.js";
6262
// Mock implementation layer
6363
export { BootstrapMock, makeMockBootstrap } from "./Layer/BootstrapMock.js";
6464

65+
// Minimal layer: only Telemetry + Bootstrap. Individual stages fail gracefully
66+
// when their dependencies (IPC, Mountain, Environment, etc.) are unavailable.
67+
// The full layer stack (ElectronBaseLayer) requires IPC which may die if
68+
// __TAURI__ isn't injected yet. The Bootstrap is diagnostic, not critical path.
69+
const BootstrapRunLayer = TelemetryLive.pipe(Layer.provideMerge(BootstrapLive));
70+
6571
export const runBootstrap = (
6672
options?: import("./Type/BootstrapType.js").BootstrapOptions,
6773
) =>
6874
Effect.gen(function* () {
6975
const bootstrap = yield* BootstrapTag;
7076
return yield* bootstrap.run(options);
71-
}).pipe(
72-
Effect.provide(TelemetryLive.pipe(Layer.provideMerge(BootstrapLive))),
73-
);
77+
}).pipe(Effect.provide(BootstrapRunLayer));

Source/Function/DevLog.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* @module Function/DevLog
3+
*
4+
* Tag-filtered development logging for the browser side.
5+
*
6+
* ## Control
7+
* Set `window.__LAND_DEV_LOG` in the browser console or pass via config:
8+
* ```js
9+
* window.__LAND_DEV_LOG = "vfs,ipc"; // only VFS + IPC
10+
* window.__LAND_DEV_LOG = "all"; // everything
11+
* delete window.__LAND_DEV_LOG; // off (default)
12+
* ```
13+
*
14+
* Or set `LAND_DEV_LOG` in localStorage for persistence:
15+
* ```js
16+
* localStorage.setItem("LAND_DEV_LOG", "config,folder");
17+
* ```
18+
*
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
28+
*/
29+
30+
let CachedTags: string[] | null = null;
31+
32+
const GetEnabledTags = (): string[] => {
33+
if (CachedTags !== null) return CachedTags;
34+
const Raw =
35+
(window as any).__LAND_DEV_LOG ??
36+
(typeof localStorage !== "undefined"
37+
? localStorage.getItem("LAND_DEV_LOG")
38+
: null);
39+
CachedTags = Raw
40+
? String(Raw)
41+
.split(",")
42+
.map((S: string) => S.trim().toLowerCase())
43+
: [];
44+
return CachedTags;
45+
};
46+
47+
const IsEnabled = (Tag: string): boolean => {
48+
const Tags = GetEnabledTags();
49+
if (Tags.length === 0) return false;
50+
const Lower = Tag.toLowerCase();
51+
return Tags.some((T) => T === "all" || T === Lower);
52+
};
53+
54+
/**
55+
* Tagged development log. Only prints if the tag is enabled.
56+
*
57+
* @example
58+
* DevLog("VFS", "stat", path, result);
59+
* DevLog("CONFIG", "resolveConfiguration folderUri:", folderUri);
60+
*/
61+
const DevLog = (Tag: string, ...Args: unknown[]): void => {
62+
if (IsEnabled(Tag)) {
63+
console.log(`[DEV:${Tag.toUpperCase()}]`, ...Args);
64+
}
65+
};
66+
67+
/** Force-reset the cache (call after changing window.__LAND_DEV_LOG). */
68+
DevLog.reset = () => {
69+
CachedTags = null;
70+
};
71+
72+
export default DevLog;

Source/Function/Install/Function/ResolveConfiguration.ts

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
import type { ISandboxConfiguration } from "@codeeditorland/output/vs/base/parts/sandbox/common/sandboxTypes";
1212

13+
import DevLog from "../../DevLog.js";
14+
1315
/**
1416
* Resolves the VSCode sandbox configuration.
1517
* Returns ISandboxConfiguration (for browser workbench) but includes
@@ -25,13 +27,83 @@ export async function ResolveConfiguration(): Promise<ISandboxConfiguration> {
2527
// Strip origin from FileRoot for appRoot (workbench.js prepends vscode-file://)
2628
const AppRoot = FileRoot.replace(/^https?:\/\/[^/]+/, "");
2729

30+
// Fetch real Tauri paths from Mountain
31+
let Paths = { userDataDir: "", logsPath: "", homeDir: "/", tmpDir: "/tmp" };
32+
try {
33+
const Invoke =
34+
(window as any).__TAURI__?.core?.invoke ??
35+
(window as any).__TAURI__?.invoke;
36+
if (typeof Invoke === "function") {
37+
Paths = await Invoke("MountainIPCInvoke", {
38+
method: "nativeHost:getEnvironmentPaths",
39+
params: [],
40+
});
41+
}
42+
} catch (Error) {
43+
DevLog("config", "MountainIPCInvoke failed:", Error);
44+
}
45+
46+
DevLog("config", "paths:", JSON.stringify(Paths));
47+
48+
// Pass LAND_DEV_LOG from Mountain environment to browser.
49+
// The Tauri IPC returns the env var; set it on window so DevLog picks it up.
50+
if ((Paths as any).devLog) {
51+
(window as any).__LAND_DEV_LOG = (Paths as any).devLog;
52+
DevLog.reset();
53+
}
54+
55+
// Read ?folder= from URL (set by pickFolderAndOpen navigation)
56+
const FolderParam = new URLSearchParams(window.location.search).get(
57+
"folder",
58+
);
59+
const FolderUri = FolderParam
60+
? { scheme: "file", path: FolderParam, authority: "" }
61+
: undefined;
62+
63+
// ISingleFolderWorkspaceIdentifier for the Electron (desktop) workbench.
64+
// The browser workbench reads `folderUri` but DesktopMain reads `workspace`.
65+
// reviveIdentifier() in desktop.main.ts calls URI.revive() on workspace.uri.
66+
const Workspace = FolderUri
67+
? {
68+
id: Array.from(FolderParam)
69+
.reduce(
70+
(Hash, Character) =>
71+
((Hash << 5) - Hash + Character.charCodeAt(0)) | 0,
72+
0,
73+
)
74+
.toString(16)
75+
.replace("-", ""),
76+
uri: FolderUri,
77+
}
78+
: undefined;
79+
80+
DevLog("config", "url:", window.location.href);
81+
DevLog("config", "folderUri:", JSON.stringify(FolderUri));
82+
DevLog("config", "workspace:", JSON.stringify(Workspace));
83+
84+
// Session timestamp for logs subdirectory
85+
const Now = new Date();
86+
const SessionTimestamp = [
87+
Now.getFullYear(),
88+
String(Now.getMonth() + 1).padStart(2, "0"),
89+
String(Now.getDate()).padStart(2, "0"),
90+
"T",
91+
String(Now.getHours()).padStart(2, "0"),
92+
String(Now.getMinutes()).padStart(2, "0"),
93+
String(Now.getSeconds()).padStart(2, "0"),
94+
].join("");
95+
const LogsLocation = Paths.logsPath
96+
? `${Paths.logsPath}/${SessionTimestamp}`
97+
: undefined;
98+
2899
return {
29100
windowId: 1,
30101
appRoot: AppRoot,
31102
userEnv: {
32-
PATH: "/usr/bin:/bin",
33-
HOME: "/",
103+
PATH: "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin",
104+
HOME: Paths.homeDir || "/",
34105
VSCODE_DEV: "true",
106+
USER: Paths.homeDir?.split("/").pop() || "user",
35107
},
36108
product: {
37109
nameShort: "VSCode Wind",
@@ -188,8 +260,20 @@ export async function ResolveConfiguration(): Promise<ISandboxConfiguration> {
188260
},
189261
},
190262
os: { release: "24.0.0" },
263+
264+
// Real paths from Mountain (Tauri PathResolver)
265+
homeDir: Paths.homeDir ? `file://${Paths.homeDir}` : undefined,
266+
tmpDir: Paths.tmpDir ? `file://${Paths.tmpDir}` : undefined,
267+
userDataDir: Paths.userDataDir
268+
? `file://${Paths.userDataDir}`
269+
: undefined,
270+
logsPath: LogsLocation ? `file://${LogsLocation}` : undefined,
271+
272+
// Workspace — set from ?folder= URL param
273+
// folderUri is used by the browser workbench; workspace by the Electron workbench.
274+
folderUri: FolderUri,
275+
workspace: Workspace,
191276
backupPath: undefined,
192-
workspace: undefined,
193277
fullscreen: false,
194278
policiesData: undefined,
195279
filesToOpenOrCreate: undefined,
@@ -199,8 +283,8 @@ export async function ResolveConfiguration(): Promise<ISandboxConfiguration> {
199283
colorScheme: { dark: true, highContrast: false },
200284
autoDetectHighContrast: true,
201285
autoDetectColorScheme: false,
202-
isInitialStartup: false,
286+
isInitialStartup: !FolderParam,
203287
perfMarks: [],
204288
accessibilitySupport: false,
205-
} as ISandboxConfiguration & Record<string, unknown>;
289+
} as unknown as ISandboxConfiguration & Record<string, unknown>;
206290
}

Source/Polyfills/ChildProcessPolyfill.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -200,29 +200,34 @@ function listenToTauri(
200200
handler: (payload: unknown) => void,
201201
): () => void {
202202
if (typeof (window as any).__TAURI__?.event?.listen === "function") {
203-
const unlistenPromise = (window as any).__TAURI__.event.listen(
204-
event,
205-
({ payload }: { payload: unknown }) => {
203+
const unlistenPromise = (window as any).__TAURI__.event
204+
.listen(event, ({ payload }: { payload: unknown }) => {
206205
handler(payload);
207-
},
208-
);
206+
})
207+
.catch(() => {
208+
// Silently ignore: event.listen may be blocked by Tauri capability
209+
// for http://localhost:* URLs during development
210+
});
209211

210212
// Return cleanup function
211213
return () => {
212-
unlistenPromise.then((unlisten: () => void) => unlisten());
214+
unlistenPromise.then((unlisten: (() => void) | undefined) =>
215+
unlisten?.(),
216+
);
213217
};
214218
}
215219

216220
if (typeof (window as any).TAURI?.event?.listen === "function") {
217-
const unlistenPromise = (window as any).TAURI.event.listen(
218-
event,
219-
({ payload }: { payload: unknown }) => {
221+
const unlistenPromise = (window as any).TAURI.event
222+
.listen(event, ({ payload }: { payload: unknown }) => {
220223
handler(payload);
221-
},
222-
);
224+
})
225+
.catch(() => {});
223226

224227
return () => {
225-
unlistenPromise.then((unlisten: () => void) => unlisten());
228+
unlistenPromise.then((unlisten: (() => void) | undefined) =>
229+
unlisten?.(),
230+
);
226231
};
227232
}
228233

Source/Polyfills/SharedProcessProxy.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -115,28 +115,33 @@ function listenToTauri(
115115
handler: (payload: unknown) => void,
116116
): () => void {
117117
if (typeof (window as any).__TAURI__?.event?.listen === "function") {
118-
const unlistenPromise = (window as any).__TAURI__.event.listen(
119-
event,
120-
({ payload }: { payload: unknown }) => {
118+
const unlistenPromise = (window as any).__TAURI__.event
119+
.listen(event, ({ payload }: { payload: unknown }) => {
121120
handler(payload);
122-
},
123-
);
121+
})
122+
.catch(() => {
123+
// Silently ignore: event.listen may be blocked by Tauri capability
124+
// for http://localhost:* URLs during development
125+
});
124126

125127
return () => {
126-
unlistenPromise.then((unlisten: () => void) => unlisten());
128+
unlistenPromise.then((unlisten: (() => void) | undefined) =>
129+
unlisten?.(),
130+
);
127131
};
128132
}
129133

130134
if (typeof (window as any).TAURI?.event?.listen === "function") {
131-
const unlistenPromise = (window as any).TAURI.event.listen(
132-
event,
133-
({ payload }: { payload: unknown }) => {
135+
const unlistenPromise = (window as any).TAURI.event
136+
.listen(event, ({ payload }: { payload: unknown }) => {
134137
handler(payload);
135-
},
136-
);
138+
})
139+
.catch(() => {});
137140

138141
return () => {
139-
unlistenPromise.then((unlisten: () => void) => unlisten());
142+
unlistenPromise.then((unlisten: (() => void) | undefined) =>
143+
unlisten?.(),
144+
);
140145
};
141146
}
142147

Source/Service/TauriMainProcessService.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ import type {
2424
IServerChannel,
2525
} from "@codeeditorland/output/vs/base/parts/ipc/common/ipc";
2626

27+
// Inline DevLog — can't import from ../Function/ because this file is served
28+
// from /Static/Application/vs/platform/ipc/ where relative imports break.
29+
const DevLog = (Tag: string, ...Args: unknown[]): void => {
30+
const Filter = (window as any).__LAND_DEV_LOG;
31+
if (!Filter) return;
32+
const Lower = Tag.toLowerCase();
33+
if (
34+
Filter === "all" ||
35+
String(Filter)
36+
.split(",")
37+
.some((T: string) => T.trim().toLowerCase() === Lower)
38+
) {
39+
console.log(`[DEV:${Tag.toUpperCase()}]`, ...Args);
40+
}
41+
};
42+
2743
// ============================================================================
2844
// Channel → Mountain Route Mapping
2945
// ============================================================================
@@ -170,6 +186,8 @@ class TauriChannel implements IChannel {
170186
Arg?: unknown,
171187
_CancellationToken?: unknown,
172188
): Promise<T> {
189+
DevLog("ipc", `${this.ChannelName}.${Command}`);
190+
173191
// Fire-and-forget channels
174192
if (FireAndForgetChannels.has(this.ChannelName)) {
175193
// Still send to Mountain but don't await
@@ -200,6 +218,32 @@ class TauriChannel implements IChannel {
200218

201219
try {
202220
const Result = await InvokeMountain(MountainMethod, Params);
221+
// File read commands return { buffer: number[] } from Mountain.
222+
// VS Code's IPCFileSystemProvider does: `const buf = await call<VSBuffer>('readFile', uri); return buf.buffer;`
223+
// So `buf.buffer` must return a Uint8Array (not an ArrayBuffer).
224+
// Return a VSBuffer-shaped object: { buffer: Uint8Array, byteLength: number }.
225+
if (
226+
FileSystemChannels.has(this.ChannelName) &&
227+
(Command === "readFile" || Command === "read")
228+
) {
229+
const Raw = Result as
230+
| { buffer: number[] }
231+
| number[]
232+
| null
233+
| undefined;
234+
if (Raw !== null && Raw !== undefined) {
235+
const Arr = Array.isArray(Raw)
236+
? Raw
237+
: (Raw as { buffer: number[] }).buffer;
238+
if (Array.isArray(Arr)) {
239+
const Bytes = new Uint8Array(Arr);
240+
return {
241+
buffer: Bytes,
242+
byteLength: Bytes.byteLength,
243+
} as unknown as T;
244+
}
245+
}
246+
}
203247
return Result as T;
204248
} catch (RawError) {
205249
// File system errors must be RETHROWN so VS Code's

Target/Effect/Bootstrap/index.js

Lines changed: 4 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)