Merge branch 'main' into fix/cctv-missing-numbers

This commit is contained in:
Sameh Khamis committed 2026-10-16 15:34:29 -07:00
commit cab69d282b
8 files changed
+107 -13

No files matched your search

+4
View File
@@ -0,0 +1,4 @@
- On Windows, `npm run doctor` and the Pinokio install no longer print Node's
DEP0190 warning ("Passing args to a child process with shell option true can
lead to security vulnerabilities"): npm.cmd's fixed subcommand is now part of
the shell command string instead of an argument array (quanticsoul4772, #1057).
@@ -0,0 +1,3 @@
- Release superseded Google Street View open waits and suppress metadata
requests whose viewer was unmounted while the service loaded (rudycelekli,
#1117).
+4 -1
View File
@@ -3478,7 +3478,10 @@ controls (the address goes in the caption); a pose goes out at once and
again when the panorama's month, photographer and address arrive (one again when the panorama's month, photographer and address arrive (one
successful lookup per panorama per session, seeded by the nearest lookup; successful lookup per panorama per session, seeded by the nearest lookup;
a details lookup that fails or takes over ten seconds waits five seconds a details lookup that fails or takes over ten seconds waits five seconds
before a view event can retry, and an unmount drops lookups in flight). A key before a view event can retry, and an unmount drops lookups in flight). Opening
another panorama settles the previous open before registering its wait; closing
then releases the current wait immediately. A details lookup released while its
service loads makes no panorama request after that service becomes available. A key
Google refuses (`gm_authFailure`: the API not enabled, a referrer it does Google refuses (`gm_authFailure`: the API not enabled, a referrer it does
not allow, billing off) reads KEY REJECTED and fails a pending open at not allow, billing off) reads KEY REJECTED and fails a pending open at
once; a lookup or open Google never answers fails after 10 or 20 s. In CI once; a lookup or open Google never answers fails after 10 or 20 s. In CI
+2 -2
View File
@@ -24,8 +24,8 @@ export function runChecked(command, args, { shell = false } = {}) {
export function installPinokioDependencies() { export function installPinokioDependencies() {
applyPinokioEnvironment(); applyPinokioEnvironment();
rmSync(READY_FILE, { force: true }); rmSync(READY_FILE, { force: true });
const npm = npmProcessSpec(); const npm = npmProcessSpec(['ci']);
runChecked(npm.command, ['ci'], { shell: npm.shell }); runChecked(npm.command, npm.args, { shell: npm.shell });
// Pinokio starts Vite directly and loads only its ENVIRONMENT file plus the // Pinokio starts Vite directly and loads only its ENVIRONMENT file plus the
// normal dotenv ladder. Unlike dev-fresh.sh, it does not import macOS // normal dotenv ladder. Unlike dev-fresh.sh, it does not import macOS
+11 -6
View File
@@ -76,10 +76,15 @@ export function hasRequiredDependencies(rootDir = ROOT) {
} }
} }
/** Return the npm command and spawn mode required by the target platform. */ /**
export function npmProcessSpec(platform = process.platform) { * Return the npm command, arguments and spawn mode for the target platform.
const windows = platform === 'win32'; * Windows must run npm.cmd through a shell, and Node 24 deprecates passing an
return { command: windows ? 'npm.cmd' : 'npm', shell: windows }; * argument array with `shell: true` (DEP0190), so there the arguments, which
* are fixed npm subcommands, join the command string.
*/
export function npmProcessSpec(args, platform = process.platform) {
if (platform !== 'win32') return { command: 'npm', args, shell: false };
return { command: ['npm.cmd', ...args].join(' '), args: [], shell: true };
} }
/** Read one key from Vite's dotenv file ladder without depending on Vite. */ /** Read one key from Vite's dotenv file ladder without depending on Vite. */
@@ -190,8 +195,8 @@ export function buildCapabilitySummary(
export function inspectSetup({ includeKeychain = true, authoritativeEnvironment = false, rootDir = ROOT } = {}) { export function inspectSetup({ includeKeychain = true, authoritativeEnvironment = false, rootDir = ROOT } = {}) {
const node = classifyNodeVersion(); const node = classifyNodeVersion();
const npm = npmProcessSpec(); const npm = npmProcessSpec(['--version']);
const npmResult = spawnSync(npm.command, ['--version'], { const npmResult = spawnSync(npm.command, npm.args, {
encoding: 'utf8', encoding: 'utf8',
shell: npm.shell, shell: npm.shell,
}); });
@@ -97,6 +97,8 @@ export function createGoogleViewer({ loader, getService, render } = {}) {
service ||= getService service ||= getService
? await getService() ? await getService()
: new (await ensureLibrary()).StreetViewService(); : new (await ensureLibrary()).StreetViewService();
if (!current())
throw new Error('Street View details lookup was released');
return service.getPanorama({ pano: panoId }); return service.getPanorama({ pano: panoId });
})().then(resolve, reject); })().then(resolve, reject);
}); });
@@ -236,6 +238,7 @@ export function createGoogleViewer({ loader, getService, render } = {}) {
const panoId = String(imageId); const panoId = String(imageId);
if (!host) throw new Error('Street View is not mounted'); if (!host) throw new Error('Street View is not mounted');
if (loader.authFailed()) throw new Error(KEY_REJECTED_MESSAGE); if (loader.authFailed()) throw new Error(KEY_REJECTED_MESSAGE);
cancelShown?.();
pendingOpen = panoId; pendingOpen = panoId;
const instance = await ensurePanorama(host); const instance = await ensurePanorama(host);
if (pendingOpen !== panoId) return; if (pendingOpen !== panoId) return;
@@ -403,3 +403,69 @@ test('a remount looks up again a panorama whose lookup hung', async () => {
await settle(); await settle();
assert.equal(poses.length, count); assert.equal(poses.length, count);
}); });
test('superseded panorama waits cannot steal the current close cleanup', async (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] });
const { OPEN_TIMEOUT_MS } = await import('./policy.js');
const flush = async () => {
for (let i = 0; i < 10; i++) await Promise.resolve();
};
const { fake, loader, viewer, host } = setup();
await viewer.mount(host);
const [panorama] = fake.built;
panorama.setPano = (id) => {
panorama.pano = id;
};
const first = viewer.open('pano-a');
await flush();
const second = viewer.open('pano-b');
await flush();
panorama.fire('position_changed'); // new panorama has not reached OK
await flush();
viewer.close();
const remaining = loader.listenerCount();
t.mock.timers.tick(OPEN_TIMEOUT_MS);
await Promise.allSettled([first, second]);
assert.equal(
remaining,
0,
'close disposes every pending open without waiting 20 seconds',
);
assert.equal(panorama.visible, false);
});
test('unmount during details-service acquisition makes no later billed lookup', async () => {
const fake = fakeStreetViewLibrary({ panoramas: PANORAMAS });
const loader = fakeMapsLoader(fake.library);
const { createGoogleProvider } = await import('./index.js');
let release;
const acquiring = new Promise((resolve) => {
release = resolve;
});
const importLibrary = loader.importLibrary.bind(loader);
let imports = 0;
loader.importLibrary = async (name) => {
if (++imports > 1) await acquiring;
return importLibrary(name);
};
const provider = createGoogleProvider({
getApiKey: () => 'fixture-key',
loader,
});
const instance = provider.create({
notify() {},
actions: { reportError() {} },
});
const viewer = instance.viewer;
await viewer.mount(fakeHost());
await viewer.open('pano-a');
viewer.unmount();
release();
for (let i = 0; i < 10; i++) await Promise.resolve();
assert.equal(
fake.lookups.length,
0,
'teardown invalidates the request before calling getPanorama',
);
instance.destroy();
});
+14 -4
View File
@@ -69,12 +69,22 @@ test('placeholder values are never counted as configured credentials', () => {
}); });
test('doctor selects a Windows-safe npm process without changing Unix behavior', () => { test('doctor selects a Windows-safe npm process without changing Unix behavior', () => {
assert.deepEqual(npmProcessSpec('win32'), { // Windows passes no argument array with shell: true (Node DEP0190).
command: 'npm.cmd', assert.deepEqual(npmProcessSpec(['ci'], 'win32'), {
command: 'npm.cmd ci',
args: [],
shell: true, shell: true,
}); });
assert.deepEqual(npmProcessSpec('darwin'), { command: 'npm', shell: false }); assert.deepEqual(npmProcessSpec(['--version'], 'darwin'), {
assert.deepEqual(npmProcessSpec('linux'), { command: 'npm', shell: false }); command: 'npm',
args: ['--version'],
shell: false,
});
assert.deepEqual(npmProcessSpec(['ci'], 'linux'), {
command: 'npm',
args: ['ci'],
shell: false,
});
}); });
test('doctor recognizes every OpenSky OAuth keychain alias used by dev-fresh', () => { test('doctor recognizes every OpenSky OAuth keychain alias used by dev-fresh', () => {