fix(desktop): intercept IPC probe at module layer for native Tauri

Native Tauri 2.11.5 defines window.__TAURI_INTERNALS__.invoke as a non-configurable, non-writable value property, so the prior Object.defineProperty install threw TypeError and the broad catch in startProfilingHarness silently aborted all four probes — a production drive day would capture nothing. Move interception to the @tauri-apps/api/core module layer via a Vite alias proxy that re-exports the real surface and wraps only invoke; the real core derefs the window property per call, so the seam never writes it and stays passive against native, terminal-accessor, and mockIPC shapes alike. Isolate each probe install so one failure cannot abort the rest.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
This commit is contained in:
Duncan
2026-08-17 20:08:05 -04:00
co-authored by Will Pfleger
parent 13d7916654
commit 437fe18948
6 changed files with 219 additions and 118 deletions
+75 -51
View File
@@ -16,7 +16,12 @@ import {
DRIFT_THRESHOLD_MS,
nextStallSample,
} from "@/shared/profiling/drift.ts";
import { installInvokeProbe } from "@/shared/profiling/ipc.ts";
import {
createInvokeObserver,
getInvokeObserver,
setInvokeObserver,
wrapInvoke,
} from "@/shared/profiling/ipc.ts";
import { ProfileRecorder, RING_CAPACITY } from "@/shared/profiling/recorder.ts";
function fixedClock() {
@@ -93,86 +98,105 @@ describe("classifyInput", () => {
});
});
describe("installInvokeProbe", () => {
it("returns false and stays passive when no invoke bridge exists", () => {
assert.equal(
installInvokeProbe(undefined, new Map(), () => {}),
false,
);
assert.equal(
installInvokeProbe({ invoke: 42 }, new Map(), () => {}),
false,
describe("wrapInvoke / observer registry", () => {
it("passes through untouched when no observer is registered", async () => {
setInvokeObserver(null);
const wrapped = wrapInvoke(
(cmd) => Promise.resolve(`ok:${cmd}`),
() => getInvokeObserver(),
);
assert.equal(await wrapped("ping"), "ok:ping");
});
it("records a resolved invoke once without recursing", async () => {
it("records a resolved invoke once through the registered observer", async () => {
const calls = [];
const internals = {
invoke: (cmd) => Promise.resolve(`ok:${cmd}`),
};
const pending = new Map();
let t = 0;
assert.equal(
installInvokeProbe(
internals,
new Map(),
setInvokeObserver(
createInvokeObserver(
pending,
(cmd, dur, ok) => calls.push({ cmd, dur, ok }),
() => (t += 5),
),
true,
);
const result = await internals.invoke("ping");
const wrapped = wrapInvoke(
(cmd) => Promise.resolve(`ok:${cmd}`),
() => getInvokeObserver(),
);
const result = await wrapped("ping");
assert.equal(result, "ok:ping");
assert.deepEqual(calls, [{ cmd: "ping", dur: 5, ok: true }]);
assert.equal(pending.size, 0);
setInvokeObserver(null);
});
it("records a rejected invoke and re-throws without leaking pending", async () => {
const calls = [];
const pending = new Map();
const internals = { invoke: () => Promise.reject(new Error("boom")) };
installInvokeProbe(internals, pending, (cmd, _dur, ok) =>
calls.push({ cmd, ok }),
setInvokeObserver(
createInvokeObserver(pending, (cmd, _dur, ok) => calls.push({ cmd, ok })),
);
await assert.rejects(() => internals.invoke("bad"), /boom/);
const wrapped = wrapInvoke(
() => Promise.reject(new Error("boom")),
() => getInvokeObserver(),
);
await assert.rejects(() => wrapped("bad"), /boom/);
assert.deepEqual(calls, [{ cmd: "bad", ok: false }]);
assert.equal(pending.size, 0);
setInvokeObserver(null);
});
it("preserves an accessor-backed reassignment bridge without stack overflow", async () => {
// Reproduces the terminal E2E backend descriptor shape: `invoke` is an
// accessor whose getter returns a dispatcher delegating unknown commands to
// closure `inner`, and whose setter *reassigns* `inner`. A plain
// `internals.invoke = wrapper` would route the dispatcher's fallback back
// into the wrapper and overflow the stack on the first non-terminal call.
it("starts recording only after the observer is registered", async () => {
setInvokeObserver(null);
const calls = [];
const wrapped = wrapInvoke(
(cmd) => Promise.resolve(cmd),
() => getInvokeObserver(),
);
// Module proxy wraps at load, before the harness starts: this call is a
// transparent pass-through and must not be recorded.
await wrapped("before");
setInvokeObserver(
createInvokeObserver(new Map(), (cmd) => calls.push(cmd)),
);
await wrapped("after");
assert.deepEqual(calls, ["after"]);
setInvokeObserver(null);
});
it("never touches a native non-configurable invoke property", async () => {
// Native Tauri 2.11.5 defines `window.__TAURI_INTERNALS__.invoke` as a
// non-configurable, non-writable value property; any redefine/assign
// throws and would abort the harness. The module seam only reads the value
// (as the real core module does per call) and wraps that function
// reference — it must never write the property back.
const internals = {};
let inner = null;
const native = (cmd) => Promise.resolve(`native:${cmd}`);
Object.defineProperty(internals, "invoke", {
configurable: true,
get: () => (cmd, args, opts) => {
if (cmd === "terminal_input") return Promise.resolve("term");
if (!inner) throw new Error(`no mock bridge for ${cmd}`);
return inner(cmd, args, opts);
},
set: (fn) => {
inner = fn;
},
configurable: false,
writable: false,
enumerable: true,
value: native,
});
// The real mock bridge is trapped through the setter after the harness runs.
const realBridge = (cmd) => Promise.resolve(`real:${cmd}`);
const calls = [];
assert.equal(
installInvokeProbe(internals, new Map(), (cmd, _dur, ok) =>
setInvokeObserver(
createInvokeObserver(new Map(), (cmd, _dur, ok) =>
calls.push({ cmd, ok }),
),
true,
);
// App code trapping the invoke after the harness (as mockIPC does) must not
// clobber the probe: the harness redefined `invoke` as a data property.
inner = realBridge;
// The real core module derefs the property per call; the wrapper closes
// over the function value, exactly like tauriCoreProxy.ts.
const wrapped = wrapInvoke(internals.invoke, () => getInvokeObserver());
const result = await wrapped("go");
const result = await internals.invoke("non_terminal");
assert.equal(result, "real:non_terminal");
assert.deepEqual(calls, [{ cmd: "non_terminal", ok: true }]);
assert.equal(result, "native:go");
assert.deepEqual(calls, [{ cmd: "go", ok: true }]);
// The forbidden property is untouched and still non-configurable.
const descriptor = Object.getOwnPropertyDescriptor(internals, "invoke");
assert.equal(descriptor.configurable, false);
assert.equal(descriptor.value, native);
setInvokeObserver(null);
});
});
+34 -27
View File
@@ -5,8 +5,9 @@
// 1. main-thread stalls (timer drift, foreground-only) + input latency
// (receipt → next paint, with pre-dispatch queue delay split out)
// 2. Tauri IPC duration/outcome + a >10s pending-invoke watchdog (deadlocks).
// The invoke bridge is redefined as a data property (see ipc.ts) so an
// accessor-backed bridge is preserved without looping into the wrapper.
// Intercepted at the `@tauri-apps/api/core` module layer (see
// tauriCoreProxy.ts) so it works against native Tauri's non-configurable
// invoke property — no window property is ever written.
// 3. relay REQ→EOSE (history fetch) and publish send→ack round-trips
// 4. periodic accumulator census (observer store, query cache, DOM nodes)
//
@@ -28,7 +29,8 @@ import {
PENDING_INVOKE_MS,
PENDING_SCAN_MS,
type PendingInvoke,
installInvokeProbe,
createInvokeObserver,
setInvokeObserver,
} from "@/shared/profiling/ipc";
import {
FLUSH_INTERVAL_MS,
@@ -96,22 +98,16 @@ function installRelayProbe(rec: ProfileRecorder): void {
}
}
type TauriInternals = {
invoke: (cmd: string, args?: unknown, opts?: unknown) => Promise<unknown>;
};
function installIpcProbe(rec: ProfileRecorder): void {
const internals = (
window as unknown as { __TAURI_INTERNALS__?: TauriInternals }
).__TAURI_INTERNALS__;
// Track outstanding invokes so a hung command surfaces via the watchdog.
const pending = new Map<number, PendingInvoke>();
const installed = installInvokeProbe(internals, pending, (cmd, dur, ok) =>
rec.record({ type: "ipc", cmd, dur, ok }),
// The core module proxy wraps `invoke` at load; registering the observer
// here begins recording. No window property is touched — see ipc.ts.
setInvokeObserver(
createInvokeObserver(pending, (cmd, dur, ok) =>
rec.record({ type: "ipc", cmd, dur, ok }),
),
);
if (!installed) {
return;
}
window.setInterval(() => {
const now = performance.now();
@@ -211,7 +207,9 @@ function installCensusProbe(
/**
* Start the harness once, wiring all four probes and the JSONL sink. Idempotent
* and non-throwing: profiling must never take down the app it measures.
* and non-throwing: profiling must never take down the app it measures. Each
* probe is isolated — one failing to install must not abort the others or the
* flush wiring.
*/
export function startProfilingHarness(queryClient: QueryClient): void {
if (started) {
@@ -219,21 +217,30 @@ export function startProfilingHarness(queryClient: QueryClient): void {
}
started = true;
try {
const sid = `session-${Date.now()}`;
const rec = new ProfileRecorder(sid, async (lines) => {
await invoke("append_profiling_log", { fileStem: sid, lines });
});
const sid = `session-${Date.now()}`;
const rec = new ProfileRecorder(sid, async (lines) => {
await invoke("append_profiling_log", { fileStem: sid, lines });
});
installIpcProbe(rec);
installRelayProbe(rec);
installStallProbe(rec);
installInputProbe(rec);
installCensusProbe(rec, queryClient);
// Install each probe under its own guard so a single failure can't silently
// collapse the rest of the capture.
safely(() => installIpcProbe(rec));
safely(() => installRelayProbe(rec));
safely(() => installStallProbe(rec));
safely(() => installInputProbe(rec));
safely(() => installCensusProbe(rec, queryClient));
safely(() => {
window.setInterval(() => void rec.flush(), FLUSH_INTERVAL_MS);
window.addEventListener("pagehide", () => void rec.flush());
});
}
/** Run harness setup work, swallowing any failure so it can't take down the app. */
function safely(fn: () => void): void {
try {
fn();
} catch {
// Never let harness setup break the app being profiled.
// A probe that cannot install is skipped; the others still run.
}
}
+63 -40
View File
@@ -1,7 +1,9 @@
// Temporary renderer profiling harness (never merges) — IPC invoke probe.
// Temporary renderer profiling harness (never merges) — IPC invoke probe core.
//
// The interception logic lives here (not harness.ts) so it can be unit-tested
// without harness.ts's module-load Tauri/store imports, matching drift.ts.
// Pure, Tauri-free logic so it is unit-testable without harness.ts's
// module-load Tauri/store imports (matching drift.ts). The module seam that
// actually wraps `@tauri-apps/api/core`'s `invoke` lives in tauriCoreProxy.ts;
// this file only decides what to record.
/** A command outstanding >10s is logged with its age — the deadlock detector. */
export const PENDING_INVOKE_MS = 10_000;
@@ -14,45 +16,73 @@ export type InvokeFn = (
opts?: unknown,
) => Promise<unknown>;
export type IpcInternals = { invoke: InvokeFn };
export type PendingInvoke = { cmd: string; startedAt: number };
/**
* Wrap `__TAURI_INTERNALS__.invoke` to record each call's duration/outcome,
* tracking outstanding calls in `pending` for the watchdog. Returns `false`
* when no invoke bridge is present (the harness stays passive).
*
* The wrapper is installed as a **data property** via `Object.defineProperty`,
* never `internals.invoke = wrapper`. The terminal E2E backend defines `invoke`
* as an accessor whose getter returns a dispatcher that falls back to a closure
* variable and whose setter *reassigns that fallback*; a plain assignment would
* therefore route the bridge's fallback back into this wrapper and overflow the
* stack on the first non-terminal invoke. Reading the getter once (to bind the
* current implementation) and redefining `invoke` as a value preserves any
* accessor- or reassignment-backed bridge without looping into ourselves.
* Observes one invoke: `begin(cmd)` marks it outstanding and returns a `settle`
* that records duration/outcome and clears it.
*/
export function installInvokeProbe(
internals: IpcInternals | undefined,
export type InvokeObserver = { begin: (cmd: string) => (ok: boolean) => void };
/**
* Build an observer that tracks each call in `pending` (for the watchdog) and
* records an `ipc` result on settle.
*/
export function createInvokeObserver(
pending: Map<number, PendingInvoke>,
record: (cmd: string, dur: number, ok: boolean) => void,
now: () => number = () => performance.now(),
): boolean {
if (!internals || typeof internals.invoke !== "function") {
return false;
}
const original = internals.invoke.bind(internals);
): InvokeObserver {
let seq = 0;
return {
begin(cmd) {
const id = seq++;
const startedAt = now();
pending.set(id, { cmd, startedAt });
return (ok) => {
pending.delete(id);
record(cmd, now() - startedAt, ok);
};
},
};
}
const wrapper: InvokeFn = (cmd, args, opts) => {
const id = seq++;
const startedAt = now();
pending.set(id, { cmd, startedAt });
const settle = (ok: boolean) => {
pending.delete(id);
record(cmd, now() - startedAt, ok);
};
return original(cmd, args, opts).then(
// Module-level observer registry. The core proxy wraps `invoke` at module load
// (before the harness starts), but only reports once the harness registers its
// observer. Until then the wrapper is a transparent pass-through.
let activeObserver: InvokeObserver | null = null;
/** Register the observer that live invoke calls report to. */
export function setInvokeObserver(observer: InvokeObserver | null): void {
activeObserver = observer;
}
/** The currently registered observer, or `null` when the harness is off. */
export function getInvokeObserver(): InvokeObserver | null {
return activeObserver;
}
/**
* Wrap a real `invoke` so each call routes through the current observer (or
* passes straight through when none is registered). The wrapper never touches
* `window.__TAURI_INTERNALS__`: native Tauri defines `invoke` as a
* non-configurable, non-writable value property, so redefining it throws and
* would abort the whole harness. Intercepting at the module layer instead keeps
* the probe passive against native, terminal-accessor, and mockIPC shapes
* alike — the real core module dereferences the window property per call, so a
* module wrapper sees every call without owning the property.
*/
export function wrapInvoke(
real: InvokeFn,
getObserver: () => InvokeObserver | null,
): InvokeFn {
return (cmd, args, opts) => {
const observer = getObserver();
if (!observer) {
return real(cmd, args, opts);
}
const settle = observer.begin(cmd);
return real(cmd, args, opts).then(
(value) => {
settle(true);
return value;
@@ -63,11 +93,4 @@ export function installInvokeProbe(
},
);
};
Object.defineProperty(internals, "invoke", {
configurable: true,
writable: true,
value: wrapper,
});
return true;
}
@@ -0,0 +1,33 @@
// Temporary renderer profiling harness (never merges) — @tauri-apps/api/core
// module proxy.
//
// Vite aliases `@tauri-apps/api/core` to this module (see vite.config.ts), so
// every importer — all raw `@tauri-apps/api/core` consumers, `invokeTauri()`,
// and every bundled Tauri plugin — resolves here. We re-export the real core
// surface unchanged (`export *`, which carries values and types) and override
// only `invoke`; an explicit named export shadows the star re-export of the
// same name.
//
// The real module is imported through the `@tauri-core-impl` alias (pointing at
// the actual core module) so this proxy's own import does not re-enter the
// alias. The wrapper reports to the harness's observer once registered and is a
// transparent pass-through until then. Crucially it never writes to
// `window.__TAURI_INTERNALS__`, which native Tauri defines as a
// non-configurable value property — the only interception seam that survives
// the production runtime, since the real core dereferences that property per
// call.
import { invoke as realInvoke } from "@tauri-core-impl";
import { getInvokeObserver, wrapInvoke } from "@/shared/profiling/ipc";
export * from "@tauri-core-impl";
export const invoke = wrapInvoke(
realInvoke as (
cmd: string,
args?: unknown,
opts?: unknown,
) => Promise<unknown>,
getInvokeObserver,
) as typeof realInvoke;
+1
View File
@@ -7,6 +7,7 @@
"skipLibCheck": true,
"paths": {
"@/*": ["./src/*"],
"@tauri-core-impl": ["./node_modules/@tauri-apps/api/core"],
"@features-manifest": ["../preview-features.json"],
"@model-capabilities-manifest": ["../scripts/model-capabilities.json"]
},
+13
View File
@@ -23,6 +23,19 @@ export default defineConfig(async () => ({
],
resolve: {
alias: {
// Temporary profiling harness (never merges): route every
// `@tauri-apps/api/core` importer (raw consumers, invokeTauri, and every
// bundled Tauri plugin) through a proxy that wraps `invoke`. The proxy
// reaches the real module via `@tauri-core-impl` so this alias does not
// re-fire. Exact string match — plugin subpaths are unaffected.
"@tauri-apps/api/core": path.resolve(
__dirname,
"./src/shared/profiling/tauriCoreProxy.ts",
),
"@tauri-core-impl": path.resolve(
__dirname,
"./node_modules/@tauri-apps/api/core.js",
),
"@": "/src",
"@features-manifest": path.resolve(__dirname, "../preview-features.json"),
"@model-capabilities-manifest": path.resolve(