Skip to content

Commit 58add8d

Browse files
Eric Migicovskyclaude
andcommitted
qemu-wasm: log geolocation and fetch outcomes from app JS
A silent OS-level location denial or a doubly-failed weather fetch was invisible; wrap the sandbox geolocation methods and log XHR/fetch failures and proxy fallbacks into the page console. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pi1WErefJ7c6kXodajm25M Signed-off-by: Eric Migicovsky <eric@repebble.com>
1 parent c7391d3 commit 58add8d

4 files changed

Lines changed: 60 additions & 8 deletions

File tree

tools/qemu-wasm/site-dist/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,7 @@ <h1>PebbleOS Emulator</h1>
594594
installer = new AppInstaller(phone, onProgress, (m) => say(m));
595595
const am = new AppMessageClient(phone, jsLog);
596596
pkjs = new PkjsRuntime(phone, am, {
597-
createSandbox: makeIframeSandbox(PKJS_PROXY),
597+
createSandbox: makeIframeSandbox(PKJS_PROXY, jsLog),
598598
storage: scopedStorage,
599599
tokenStore: window.localStorage,
600600
openUrl: openConfigUrl,

tools/qemu-wasm/site-dist/pkjs-runtime.js

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const uuidKey = (u) => [...u].map((b) => b.toString(16).padStart(2, '0')).join('
1616
// fallback: requests are tried direct first; on a network failure
1717
// (typically CORS) the same request is retried through proxyUrl.
1818
// Covers the subset PebbleKit JS apps use.
19-
export function makeXhrOverFetch(fetchFn, proxyUrl) {
19+
export function makeXhrOverFetch(fetchFn, proxyUrl, log = () => {}) {
2020
const proxied = (url) => proxyUrl ? proxyUrl + encodeURIComponent(url) : null;
2121

2222
async function robustFetch(url, opts) {
@@ -25,6 +25,7 @@ export function makeXhrOverFetch(fetchFn, proxyUrl) {
2525
} catch (e) {
2626
const p = /^https?:/i.test(url) && proxied(url);
2727
if (!p) throw e;
28+
log(`direct fetch blocked (${String(url).slice(0, 80)}), retrying via proxy`);
2829
return fetchFn(p, opts);
2930
}
3031
}
@@ -81,6 +82,7 @@ export function makeXhrOverFetch(fetchFn, proxyUrl) {
8182
if (timer) clearTimeout(timer);
8283
if (this._aborted && !this._timedOut) return;
8384
this.readyState = 4;
85+
log(`XHR ${this._timedOut ? 'timeout' : 'failed'}: ${String(this._url).slice(0, 80)} (${e.message})`);
8486
if (this._timedOut && this.ontimeout) this.ontimeout(e);
8587
else if (this.onerror) this.onerror(e);
8688
});
@@ -248,7 +250,7 @@ export function makeMemoryStorage() {
248250

249251
// Browser sandbox: hidden same-origin iframe. The app JS gets the page's
250252
// real fetch/XHR (patched with the proxy fallback), geolocation, etc.
251-
export function makeIframeSandbox(proxyUrl) {
253+
export function makeIframeSandbox(proxyUrl, log = () => {}) {
252254
return (globals) => {
253255
const frame = document.createElement('iframe');
254256
frame.style.display = 'none';
@@ -260,15 +262,39 @@ export function makeIframeSandbox(proxyUrl) {
260262
for (const [k, v] of Object.entries(globals)) {
261263
Object.defineProperty(w, k, { value: v, configurable: true, writable: true });
262264
}
263-
const XHR = makeXhrOverFetch(w.fetch.bind(w), proxyUrl);
265+
const XHR = makeXhrOverFetch(w.fetch.bind(w), proxyUrl, log);
264266
w.XMLHttpRequest = XHR;
265267
const nativeFetch = w.fetch.bind(w);
266268
w.fetch = (url, opts) => nativeFetch(url, opts).catch((e) => {
267269
if (proxyUrl && typeof url === 'string' && /^https?:/i.test(url)) {
270+
log(`direct fetch blocked (${url.slice(0, 80)}), retrying via proxy`);
268271
return nativeFetch(proxyUrl + encodeURIComponent(url), opts);
269272
}
270273
throw e;
271274
});
275+
// Log geolocation outcomes — a silent OS-level denial otherwise
276+
// looks identical to an app that never asked.
277+
try {
278+
const geo = w.navigator.geolocation;
279+
const origGet = geo.getCurrentPosition.bind(geo);
280+
geo.getCurrentPosition = (ok, err, opts) => {
281+
log('app requested geolocation…');
282+
origGet(
283+
(pos) => { log(`geolocation ok (±${Math.round(pos.coords.accuracy)}m)`); ok(pos); },
284+
(e) => { log(`geolocation DENIED/failed: ${e.message} (code ${e.code})`); if (err) err(e); },
285+
opts,
286+
);
287+
};
288+
const origWatch = geo.watchPosition.bind(geo);
289+
geo.watchPosition = (ok, err, opts) => {
290+
log('app watching geolocation…');
291+
return origWatch(
292+
(pos) => { log('geolocation update'); ok(pos); },
293+
(e) => { log(`geolocation DENIED/failed: ${e.message} (code ${e.code})`); if (err) err(e); },
294+
opts,
295+
);
296+
};
297+
} catch (e) { /* geolocation unavailable in this context */ }
272298
return {
273299
run: (code) => w.eval(code),
274300
dispose: () => frame.remove(),

tools/qemu-wasm/web/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,7 @@ <h1>PebbleOS Emulator</h1>
594594
installer = new AppInstaller(phone, onProgress, (m) => say(m));
595595
const am = new AppMessageClient(phone, jsLog);
596596
pkjs = new PkjsRuntime(phone, am, {
597-
createSandbox: makeIframeSandbox(PKJS_PROXY),
597+
createSandbox: makeIframeSandbox(PKJS_PROXY, jsLog),
598598
storage: scopedStorage,
599599
tokenStore: window.localStorage,
600600
openUrl: openConfigUrl,

tools/qemu-wasm/web/pkjs-runtime.js

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const uuidKey = (u) => [...u].map((b) => b.toString(16).padStart(2, '0')).join('
1616
// fallback: requests are tried direct first; on a network failure
1717
// (typically CORS) the same request is retried through proxyUrl.
1818
// Covers the subset PebbleKit JS apps use.
19-
export function makeXhrOverFetch(fetchFn, proxyUrl) {
19+
export function makeXhrOverFetch(fetchFn, proxyUrl, log = () => {}) {
2020
const proxied = (url) => proxyUrl ? proxyUrl + encodeURIComponent(url) : null;
2121

2222
async function robustFetch(url, opts) {
@@ -25,6 +25,7 @@ export function makeXhrOverFetch(fetchFn, proxyUrl) {
2525
} catch (e) {
2626
const p = /^https?:/i.test(url) && proxied(url);
2727
if (!p) throw e;
28+
log(`direct fetch blocked (${String(url).slice(0, 80)}), retrying via proxy`);
2829
return fetchFn(p, opts);
2930
}
3031
}
@@ -81,6 +82,7 @@ export function makeXhrOverFetch(fetchFn, proxyUrl) {
8182
if (timer) clearTimeout(timer);
8283
if (this._aborted && !this._timedOut) return;
8384
this.readyState = 4;
85+
log(`XHR ${this._timedOut ? 'timeout' : 'failed'}: ${String(this._url).slice(0, 80)} (${e.message})`);
8486
if (this._timedOut && this.ontimeout) this.ontimeout(e);
8587
else if (this.onerror) this.onerror(e);
8688
});
@@ -248,7 +250,7 @@ export function makeMemoryStorage() {
248250

249251
// Browser sandbox: hidden same-origin iframe. The app JS gets the page's
250252
// real fetch/XHR (patched with the proxy fallback), geolocation, etc.
251-
export function makeIframeSandbox(proxyUrl) {
253+
export function makeIframeSandbox(proxyUrl, log = () => {}) {
252254
return (globals) => {
253255
const frame = document.createElement('iframe');
254256
frame.style.display = 'none';
@@ -260,15 +262,39 @@ export function makeIframeSandbox(proxyUrl) {
260262
for (const [k, v] of Object.entries(globals)) {
261263
Object.defineProperty(w, k, { value: v, configurable: true, writable: true });
262264
}
263-
const XHR = makeXhrOverFetch(w.fetch.bind(w), proxyUrl);
265+
const XHR = makeXhrOverFetch(w.fetch.bind(w), proxyUrl, log);
264266
w.XMLHttpRequest = XHR;
265267
const nativeFetch = w.fetch.bind(w);
266268
w.fetch = (url, opts) => nativeFetch(url, opts).catch((e) => {
267269
if (proxyUrl && typeof url === 'string' && /^https?:/i.test(url)) {
270+
log(`direct fetch blocked (${url.slice(0, 80)}), retrying via proxy`);
268271
return nativeFetch(proxyUrl + encodeURIComponent(url), opts);
269272
}
270273
throw e;
271274
});
275+
// Log geolocation outcomes — a silent OS-level denial otherwise
276+
// looks identical to an app that never asked.
277+
try {
278+
const geo = w.navigator.geolocation;
279+
const origGet = geo.getCurrentPosition.bind(geo);
280+
geo.getCurrentPosition = (ok, err, opts) => {
281+
log('app requested geolocation…');
282+
origGet(
283+
(pos) => { log(`geolocation ok (±${Math.round(pos.coords.accuracy)}m)`); ok(pos); },
284+
(e) => { log(`geolocation DENIED/failed: ${e.message} (code ${e.code})`); if (err) err(e); },
285+
opts,
286+
);
287+
};
288+
const origWatch = geo.watchPosition.bind(geo);
289+
geo.watchPosition = (ok, err, opts) => {
290+
log('app watching geolocation…');
291+
return origWatch(
292+
(pos) => { log('geolocation update'); ok(pos); },
293+
(e) => { log(`geolocation DENIED/failed: ${e.message} (code ${e.code})`); if (err) err(e); },
294+
opts,
295+
);
296+
};
297+
} catch (e) { /* geolocation unavailable in this context */ }
272298
return {
273299
run: (code) => w.eval(code),
274300
dispose: () => frame.remove(),

0 commit comments

Comments
 (0)