fix: coordinate CLI and flasher map installs with a shared marker

Greptile round 7 remediation:

The CLI's advisory flock and the web flasher's Web Locks name live in
incompatible lock namespaces, so a CLI install and a browser install
on the same card could each hold "their" lock, both pass preflight on
identical slot state, and clobber each other's records (orphaning a
pack, or writing the same generation into two slots).

Both producers now share two symmetric layers:

1. An on-disk install marker (.pyxis-installing in pyxis-map/,
   token "PYXI 1 <owner> <epoch_ms>") claimed at install start via an
   atomic exclusive create, with ownership verified by read-back. A
   fresh foreign marker refuses the install before any mutation; a
   stale one (30-minute TTL; a future epoch counts as fresh under
   clock skew) is reclaimed. Release only deletes the releasee's own
   token.
2. Commit-time revalidation: activate_map_set / activateMapSet
   snapshot the raw slot and style bytes at derivation, re-read them
   immediately before the record writes, and abort if the activation
   state moved or a fresh foreign marker is present. The published
   pack is kept (device-harmless: the firmware only reads packs named
   by an active selection) and the existing retry/resume path
   converges it.

Regression tests on both producers: marker refusal before any
publication, stale-marker reclamation and release, and
commit-time-race abort with retry convergence. The in-memory flasher
FS now models getFileHandle({exclusive:true}) -> FileExistsError.

CLI: 85 passed. Flasher: 24 passed.
This commit is contained in:
Torlando committed 2026-08-29 17:54:27 +00:00
1 parent 370c5de629
commit affd5566ab
4 files changed
+634 -65

No files matched your search

+138 -21
View File
@@ -58,6 +58,106 @@ async function withInstallLock(rootDirectory, operation) {
export class MapInstallerError extends Error {}
function fail(message) { throw new MapInstallerError(message); }
// Cross-producer install marker. The CLI builder (tools/maps/build_map_pack.py)
// claims the same well-known file with the same token format before it
// publishes or activates. The browser holds a Web Locks name, which the CLI's
// flock cannot see, and the CLI holds a flock, which this tab cannot see:
// neither lock coordinates across producers, so the on-disk marker is the
// one shared coordination primitive. A marker contains: PYXI 1 <owner>
// <epoch_ms>. A marker written within INSTALL_MARKER_TTL_MS is a live
// installer; older ones are abandoned (crashed builder, closed tab) and are
// reclaimed. A future epoch (the other writer's clock ahead of ours) counts
// as fresh: refusing is the safe direction.
const INSTALL_MARKER_NAME = '.pyxis-installing';
const INSTALL_MARKER_TTL_MS = 15 * 60 * 1000;
function parseInstallMarker(text) {
const parts = String(text).trim().split(' ');
if (parts.length !== 4 || parts[0] !== 'PYXI' || parts[1] !== '1') return null;
if (!parts[2] || parts[2].length > 64) return null;
if (!/^\d+$/.test(parts[3])) return null;
return {owner: parts[2], epochMs: Number(parts[3])};
}
function installMarkerIsFresh(epochMs, nowMs = Date.now()) {
if (epochMs > nowMs) return true;
return nowMs - epochMs <= INSTALL_MARKER_TTL_MS;
}
async function readInstallMarker(pyxis) {
let handle; try { handle = await pyxis.getFileHandle(INSTALL_MARKER_NAME); }
catch (error) { if (error?.name === 'NotFoundError') return null; throw error; }
return new TextDecoder('utf-8').decode(new Uint8Array(await (await handle.getFile()).arrayBuffer()));
}
async function acquireInstallMarker(pyxis, owner) {
const token = `PYXI 1 ${owner} ${Date.now()}`;
const existing = await readInstallMarker(pyxis);
if (existing !== null) {
const parsed = parseInstallMarker(existing);
if (parsed && installMarkerIsFresh(parsed.epochMs)) {
fail('Another map installer is already running on this card; wait for it to finish and retry');
}
// Stale or malformed: reclaim, then claim fresh below.
try { await pyxis.removeEntry(INSTALL_MARKER_NAME); } catch {}
}
let handle;
try { handle = await pyxis.getFileHandle(INSTALL_MARKER_NAME, {create: true, exclusive: true}); }
catch (error) {
if (error && error.name === 'FileExistsError') {
fail('Another map installer is already running on this card; wait for it to finish and retry');
}
throw error;
}
const writable = await handle.createWritable({keepExistingData: false});
try { await writable.write(textEncoder.encode(token)); await writable.close(); }
catch (error) { try { await writable.abort(); } catch {} throw error; }
// Read-back: a foreign content means we lost a simultaneous claim race.
// Never delete a foreign marker -- our claim is void.
const actual = await readInstallMarker(pyxis);
if (actual !== token) fail('The map-install marker was contested during acquisition; wait and retry');
return token;
}
async function releaseInstallMarker(pyxis, token) {
const actual = await readInstallMarker(pyxis);
if (actual === token) { try { await pyxis.removeEntry(INSTALL_MARKER_NAME); } catch {} }
}
// Commit-time revalidation: the slot/style records were derived by
// prepareActiveMapSet; a cross-producer installer may have committed new
// activation state since. Re-reading the raw bytes immediately before the
// record writes and aborting on any change guarantees this publication never
// overwrites a newer record with one derived from stale state. The
// already-published pack is kept; a retry re-derives from the new state and
// converges (a pack no record names is device-harmless: the firmware only
// reads packs enumerated in the active selection).
async function verifyActivationState(pyxis, mapSets, styleName, markerToken, state) {
const marker = await readInstallMarker(pyxis);
if (marker !== null) {
const parsed = parseInstallMarker(marker);
if (parsed && installMarkerIsFresh(parsed.epochMs) && marker !== markerToken) {
fail('Another map installer is running on this card; wait for it to finish and retry');
}
}
const sameRecord = (actual, expected) =>
(actual === null && expected === null) ||
(actual !== null && expected !== null && equalBytes(actual, expected));
for (let index = 0; index < 2; index += 1) {
const name = `active-pack.${index}`;
let actual; try { actual = new Uint8Array(await (await (await pyxis.getFileHandle(name)).getFile()).arrayBuffer()); }
catch (error) { if (error?.name === 'NotFoundError') actual = null; else throw error; }
if (!sameRecord(actual, state.slotBytes[index])) {
fail('Active map-set records changed during installation; wait for the other installer to finish and retry');
}
}
let styleActual; try { styleActual = new Uint8Array(await (await (await mapSets.getFileHandle(styleName)).getFile()).arrayBuffer()); }
catch (error) { if (error?.name === 'NotFoundError') styleActual = null; else throw error; }
if (!sameRecord(styleActual, state.styleBytes)) {
fail('Installed style record changed during installation; wait for the other installer to finish and retry');
}
}
const MUI_STYLE_PROFILES = Object.freeze({
'osm-bright': Object.freeze({label:'OSM Bright',attribution:'(c) OpenMapTiles (c) OpenStreetMap contributors',license:'OSM ODbL; style CC-BY-4.0/BSD-3-Clause'}),
'dark-matter': Object.freeze({label:'Dark Matter',attribution:'(c) OpenMapTiles (c) OpenStreetMap contributors; style (c) CARTO',license:'OSM ODbL; style CC-BY-4.0/BSD-3-Clause (CARTO CC-BY-3.0)'}),
@@ -661,11 +761,15 @@ async function removeOwnedPack(packs,packId,pack,receiptName,receipt,entries){
async function prepareActiveMapSet(pyxis,metadata){
const slots=await Promise.all([readSelection(pyxis,'active-pack.0'),readSelection(pyxis,'active-pack.1')]);
if(slots[0]&&slots[1]&&slots[0].generation===slots[1].generation&&JSON.stringify(slots[0])!==JSON.stringify(slots[1]))fail('Conflicting active map-set records');
// Raw slot bytes at derivation time: the commit-time revalidation in
// activateMapSet aborts if they change before the record writes.
const slotBytes=[];for(let index=0;index<2;index+=1){let raw;try{raw=new Uint8Array(await (await (await pyxis.getFileHandle(`active-pack.${index}`)).getFile()).arrayBuffer());}catch(error){if(error?.name==='NotFoundError')raw=null;else throw error;}slotBytes.push(raw);}
const valid=slots.filter(Boolean);const highest=Math.max(0,...valid.map(slot=>slot.generation));if(highest===0xffffffff)fail('Active selection generation is exhausted');
const current=valid.sort((left,right)=>right.generation-left.generation)[0];
const mapSets=await getDirectory(pyxis,'map-sets');const styleName=`${metadata.mapSetId}.pmas`;
const installed=await readStyleSelection(mapSets,styleName);
if(installed&&(![2,3].includes(installed.version)||installed.mapSetId!==metadata.mapSetId||installed.attribution!==metadata.attribution))fail('Installed style record does not match selected map set');
let styleBytes=null;try{styleBytes=new Uint8Array(await (await (await mapSets.getFileHandle(styleName)).getFile()).arrayBuffer());}catch(error){if(error?.name!=='NotFoundError')throw error;}
const composition=installed||([2,3].includes(current?.version)&&current.mapSetId===metadata.mapSetId?current:null);
let packs=[];
if(composition){
@@ -675,11 +779,12 @@ async function prepareActiveMapSet(pyxis,metadata){
packs.unshift({packId:metadata.packId});
const target=!slots[0]?'active-pack.0':!slots[1]?'active-pack.1':slots[0].generation<=slots[1].generation?'active-pack.0':'active-pack.1';
const record=encodeActiveMapSet({generation:highest+1,mapSetId:metadata.mapSetId,attribution:metadata.attribution,packs});
return {target,record,packs,mapSets,styleName};
return {target,record,packs,mapSets,styleName,slotBytes,styleBytes};
}
async function activateMapSet(pyxis,metadata){
const {target,record,mapSets,styleName}=await prepareActiveMapSet(pyxis,metadata);
async function activateMapSet(pyxis,metadata,markerToken=null){
const {target,record,mapSets,styleName,slotBytes,styleBytes}=await prepareActiveMapSet(pyxis,metadata);
await verifyActivationState(pyxis,mapSets,styleName,markerToken,{slotBytes,styleBytes});
await writeVerified(mapSets,styleName,record);const installed=decodeActiveSelection(await fileBytes(mapSets,styleName));
if(installed.version!==3||installed.mapSetId!==metadata.mapSetId||installed.packs[0].packId!==metadata.packId)fail('Style map-set verification failed');
await writeVerified(pyxis,target,record);const selected=decodeActiveSelection(await fileBytes(pyxis,target));
@@ -713,26 +818,38 @@ async function installMuiZipLocked({archive,rootDirectory,metadata,onProgress=()
const report=await inspectMuiZip(archive,{onProgress});
if(report.styleId&&report.styleId!==metadata.mapSetId)fail('MUI ZIP style does not match the selected map set');
const manifest=serializeIndexlessManifest(metadata,report.minZoom,report.maxZoom,report.tileCount);const pyxis=await getDirectory(rootDirectory,'pyxis-map');
await prepareActiveMapSet(pyxis,metadata);const packs=await getDirectory(pyxis,'packs');
let existing=null;try{existing=await packs.getDirectoryHandle(metadata.packId);}catch(error){if(error?.name!=='NotFoundError')throw error;}
if(existing){
let empty=true;for await(const _entry of existing.entries()){empty=false;break;}
if(!empty){await verifyExistingPack(existing,archive,report,manifest);try{const active=await activateMapSet(pyxis,metadata);return{...report,manifestBytes:manifest.length,resumed:true,...active};}catch(error){throw new Error(`Map pack is installed and verified, but activation failed: ${error.message}`);}}
}
const pack=existing||await getDirectory(packs,metadata.packId);await verifyNamedDirectory(packs,metadata.packId,pack,[]);let published=false;const receipt=new Uint8Array(16);crypto.getRandomValues(receipt);const receiptName=`.pyxis-install-owner-${[...receipt].map(byte=>byte.toString(16).padStart(2,'0')).join('')}`;
// Claim the cross-producer install marker (shared on-disk token with the
// CLI builder; the Web Locks name above cannot coordinate with it) before
// any mutation of the card: a refusal here must not create packs/ or
// map-sets/. The marker is held until every activation record write
// completes; the commit-time revalidation in activateMapSet is the safety
// net if it is bypassed or outlived.
const markerToken=await acquireInstallMarker(pyxis,`web-${Date.now().toString(16)}-${Math.random().toString(16).slice(2)}`);
try{
await writeVerified(pack,receiptName,receipt);await verifyNamedDirectory(packs,metadata.packId,pack,[receiptName]);
const tiles=await getDirectory(pack,'tiles');
for(let index=0;index<report.entries.length;index++){const entry=report.entries[index];const zoom=await getDirectory(tiles,String(entry.zoom));const x=await getDirectory(zoom,String(entry.x));const bytes=await blobBytes(archive,entry.dataOffset,entry.dataOffset+entry.size);if(crc32(bytes)!==entry.checksum)fail(`ZIP changed during installation: ${entry.name}`);await validatePng(bytes,entry.name);await writeVerified(x,`${entry.y}.png`,bytes);onProgress({phase:'write',completed:index+1,total:report.tileCount});}
await writeVerified(pack,'manifest.pmp',manifest);parseSparseManifest(await fileBytes(pack,'manifest.pmp'));published=true;
try{await pack.removeEntry(receiptName);}catch(error){throw new Error(`Map pack is installed and verified, but its ownership receipt could not be removed: ${error.message}`);}
try{const active=await activateMapSet(pyxis,metadata);return{...report,manifestBytes:manifest.length,resumed:false,...active};}
catch(error){throw new Error(`Map pack is installed and verified, but activation failed; retry with the same ZIP and pack ID: ${error.message}`);}
}catch(error){
if(!published&&!(await removeOwnedPack(packs,metadata.packId,pack,receiptName,receipt,report.entries))){
throw new Error(`${error.message}; the incomplete pack could not be safely removed because its ownership receipt was missing or cleanup failed`);
await prepareActiveMapSet(pyxis,metadata);
const packs=await getDirectory(pyxis,'packs');
let existing=null;try{existing=await packs.getDirectoryHandle(metadata.packId);}catch(error){if(error?.name!=='NotFoundError')throw error;}
if(existing){
let empty=true;for await(const _entry of existing.entries()){empty=false;break;}
if(!empty){await verifyExistingPack(existing,archive,report,manifest);try{const active=await activateMapSet(pyxis,metadata,markerToken);return{...report,manifestBytes:manifest.length,resumed:true,...active};}catch(error){throw new Error(`Map pack is installed and verified, but activation failed: ${error.message}`);}}
}
throw error;
const pack=existing||await getDirectory(packs,metadata.packId);await verifyNamedDirectory(packs,metadata.packId,pack,[]);let published=false;const receipt=new Uint8Array(16);crypto.getRandomValues(receipt);const receiptName=`.pyxis-install-owner-${[...receipt].map(byte=>byte.toString(16).padStart(2,'0')).join('')}`;
try{
await writeVerified(pack,receiptName,receipt);await verifyNamedDirectory(packs,metadata.packId,pack,[receiptName]);
const tiles=await getDirectory(pack,'tiles');
for(let index=0;index<report.entries.length;index++){const entry=report.entries[index];const zoom=await getDirectory(tiles,String(entry.zoom));const x=await getDirectory(zoom,String(entry.x));const bytes=await blobBytes(archive,entry.dataOffset,entry.dataOffset+entry.size);if(crc32(bytes)!==entry.checksum)fail(`ZIP changed during installation: ${entry.name}`);await validatePng(bytes,entry.name);await writeVerified(x,`${entry.y}.png`,bytes);onProgress({phase:'write',completed:index+1,total:report.tileCount});}
await writeVerified(pack,'manifest.pmp',manifest);parseSparseManifest(await fileBytes(pack,'manifest.pmp'));published=true;
try{await pack.removeEntry(receiptName);}catch(error){throw new Error(`Map pack is installed and verified, but its ownership receipt could not be removed: ${error.message}`);}
try{const active=await activateMapSet(pyxis,metadata,markerToken);return{...report,manifestBytes:manifest.length,resumed:false,...active};}
catch(error){throw new Error(`Map pack is installed and verified, but activation failed; retry with the same ZIP and pack ID: ${error.message}`);}
}catch(error){
if(!published&&!(await removeOwnedPack(packs,metadata.packId,pack,receiptName,receipt,report.entries))){
throw new Error(`${error.message}; the incomplete pack could not be safely removed because its ownership receipt was missing or cleanup failed`);
}
throw error;
}
}finally{
await releaseInstallMarker(pyxis,markerToken);
}
}
+151
View File
@@ -1130,3 +1130,154 @@ def test_indexed_png_fast_path_accepts_in_range_and_rejects_out_of_range(tmp_pat
put_tile(source_bad, 0, 0, 0, bad)
with pytest.raises(tool.PackError, match="palette index out of range"):
tool.build_map_pack(source_bad, tmp_path / "sd-bad", **metadata())
def test_fresh_foreign_install_marker_is_refused_before_any_publication(
tmp_path: Path) -> None:
# A live cross-producer installer (the web flasher holds a Web Locks
# name, which this process's flock cannot see) is announced by a fresh
# on-disk marker in pyxis-map/. The builder must refuse up front,
# before staging tiles or publishing.
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 1, 0, 0)
policy = tool.STYLE_POLICIES["osm-bright"]
sd = tmp_path / "sd"
(sd / "pyxis-map").mkdir(parents=True)
(sd / "pyxis-map/.pyxis-installing").write_text(
"PYXI 1 web-deadbeef " + str(int(__import__("time").time() * 1000)))
with pytest.raises(tool.PackError, match="already running"):
tool.build_map_pack(source, sd, pack_id="pack-a", name="A",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
# Nothing was staged, published, or activated: no pack, no
# records. The empty packs/ directory is created before the
# marker check and is harmless (the firmware reads only fixed
# named paths). The foreign marker is left in place: the other
# installer is live.
assert not (sd / "pyxis-map/packs/pack-a").exists()
assert not (sd / "pyxis-map/active-pack.0").exists()
assert not (sd / "pyxis-map/map-sets").exists()
# The foreign marker was not touched: the other installer is live.
assert (sd / "pyxis-map/.pyxis-installing").is_file()
def test_stale_install_marker_is_reclaimed_and_released(tmp_path: Path) -> None:
# An abandoned installer (crashed builder, closed tab) leaves a stale
# marker. The builder reclaims it, installs, and releases it on the
# happy path.
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 1, 0, 0)
policy = tool.STYLE_POLICIES["osm-bright"]
sd = tmp_path / "sd"
(sd / "pyxis-map").mkdir(parents=True)
stale = int(__import__("time").time() * 1000) - 24 * 3600 * 1000
(sd / "pyxis-map/.pyxis-installing").write_text(f"PYXI 1 cli-{stale:x} {stale}")
tool.build_map_pack(source, sd, pack_id="pack-a", name="A",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
assert (sd / "pyxis-map/packs/pack-a/manifest.pmp").is_file()
assert (sd / "pyxis-map/active-pack.0").is_file()
# The builder released its own marker; nothing is left behind.
assert not (sd / "pyxis-map/.pyxis-installing").exists()
def test_release_never_deletes_a_foreign_marker(tmp_path: Path) -> None:
# If a later installer reclaimed the marker after ours outlasted the
# TTL, release must leave the foreign claim alone.
tool = load_tool()
sd = tmp_path / "sd"
(sd / "pyxis-map").mkdir(parents=True)
pyxis_fd = os.open(str(sd / "pyxis-map"), os.O_RDONLY)
try:
token = tool._acquire_install_marker(pyxis_fd)
# A later installer overwrote our claim.
foreign = "PYXI 1 web-cafebabe " + str(int(__import__("time").time() * 1000))
(sd / "pyxis-map/.pyxis-installing").write_text(foreign)
tool._release_install_marker(pyxis_fd, token)
assert (sd / "pyxis-map/.pyxis-installing").read_text() == foreign
finally:
os.close(pyxis_fd)
def test_commit_time_revalidation_aborts_and_retry_converges(tmp_path: Path) -> None:
# Commit-time revalidation: if a cross-producer installer commits new
# activation state after our records were derived, the publication is
# aborted before any record write. The published pack is kept (device-
# harmless while no record names it) and a retry converges the records.
tool = load_tool()
source = tmp_path / "xyz"
put_tile(source, 1, 0, 0)
policy = tool.STYLE_POLICIES["osm-bright"]
sd = tmp_path / "sd"
real_activate = tool.activate_map_set
real_verify = tool._verify_activation_state_at
def racing_activate(pyxis_fd, **kwargs):
# One-shot: fires only on the first (aborted) run.
if raced["fired"]:
real_activate(pyxis_fd, **kwargs)
return
raced["fired"] = True
# Derivation happens inside the real activate_map_set (raw slot and
# style bytes are read there); run it once so the snapshot is
# taken, then simulate a concurrent cross-producer commit landing
# between that read and the commit-time revalidation. A real
# installer commits both records (style first, slot second), so the
# simulated competitor does the same.
real_activate(pyxis_fd, **kwargs)
assert tool._read_selection_at(pyxis_fd, "active-pack.1") is None
record2 = tool.encode_active_map_set(
generation=2, map_set_id="osm-bright",
attribution=policy["attribution"], pack_ids=["other-pack", "raced-pack"])
map_sets_fd = tool._mkdir_open(pyxis_fd, "map-sets")[0]
try:
tool._write_atomic_at(map_sets_fd, "osm-bright.pmas", record2)
tool._write_atomic_at(pyxis_fd, "active-pack.0", record2)
finally:
os.close(map_sets_fd)
# Now run the commit-time revalidation for real: it must see the
# changed slot bytes (our derivation snapshot had both slots empty)
# and abort before any record write.
map_sets_fd = tool._mkdir_open(pyxis_fd, "map-sets")[0]
token = tool._read_selection_at(pyxis_fd, tool._INSTALL_MARKER_NAME).decode("ascii")
try:
real_verify(pyxis_fd, map_sets_fd,
slot_raw=[None, None], style_raw=None,
style_name="osm-bright.pmas", marker_token=token)
raise AssertionError("revalidation accepted a changed slot")
finally:
os.close(map_sets_fd)
raced = {"fired": False}
tool.activate_map_set = racing_activate
try:
with pytest.raises(tool.PackError, match="changed during installation"):
tool.build_map_pack(source, sd, pack_id="pack-a", name="A",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
finally:
tool.activate_map_set = real_activate
# The pack was published but no record was written by this run: the
# card holds exactly the raced installer's consistent gen-2 records
# (style first, slot second -- no split state from this aborted run).
assert (sd / "pyxis-map/packs/pack-a/manifest.pmp").is_file()
raced_record = tool.encode_active_map_set(
generation=2, map_set_id="osm-bright",
attribution=policy["attribution"], pack_ids=["other-pack", "raced-pack"])
assert (sd / "pyxis-map/active-pack.0").read_bytes() == raced_record
assert (sd / "pyxis-map/map-sets/osm-bright.pmas").read_bytes() == raced_record
assert not (sd / "pyxis-map/active-pack.1").exists()
# No marker is left behind.
assert not (sd / "pyxis-map/.pyxis-installing").exists()
# Retry: the pack verifies, records converge, and the new selection
# includes both packs with pack-a at the front.
result = tool.build_map_pack(source, sd, pack_id="pack-a", name="A",
attribution=policy["attribution"], source=policy["source"],
license=policy["license"], style="osm-bright", activate=True)
record3 = tool.decode_active_selection((sd / "pyxis-map/active-pack.1").read_bytes())
assert record3["generation"] == 3
assert record3["packs"] == ["pack-a", "other-pack", "raced-pack"]
assert Path(result) == sd / "pyxis-map" / "packs" / "pack-a"
+117
View File
@@ -13,6 +13,7 @@ import {
parseSparseManifest,
resolveMuiStyleProfile,
suggestMapIdentity,
MapInstallerError,
} from '../../docs/flasher/js/map-installer.js';
const PNG = Buffer.from(
@@ -143,6 +144,13 @@ class MemoryDirectoryHandle {
const current = this.children.get(name);
if (current) {
if (current.kind !== 'file') throw new DOMException('type', 'TypeMismatchError');
// File System Access API: exclusive creation of an existing entry
// fails (FileExistsError). The install-marker protocol relies on it.
if (options.exclusive) {
const error = new Error('The file already exists.');
error.name = 'FileExistsError';
throw error;
}
return current;
}
if (!options.create) throw new DOMException('missing', 'NotFoundError');
@@ -713,3 +721,112 @@ test('map-set capacity is checked before a ninth pack is published', async () =>
await assert.rejects(installMuiZip({archive,rootDirectory:root,metadata:{...metadata,packId:'pack-nine',name:'Pack Nine'}}), /active map set/i);
assert.equal(pyxis.children.has('packs'), false);
});
// --- Cross-producer install marker + commit-time revalidation (Greptile
// round-7 P1). The CLI builder claims the same on-disk marker token; the
// Web Locks name alone cannot coordinate with it. ---
const MARKER = '.pyxis-installing';
function freshMarker(owner = 'cli-0') {
return new TextEncoder().encode(`PYXI 1 ${owner} ${Date.now()}`);
}
test('a fresh foreign install marker is refused before any record is written', async () => {
const archive = storedZip([['2/1/1.png', PNG]]);
const root = new MemoryDirectoryHandle('sd');
const pyxis = await root.getDirectoryHandle('pyxis-map', {create: true});
const originalMarker = freshMarker('cli-0');
const marker = new MemoryFileHandle(MARKER, root.log);
marker.bytes = originalMarker;
pyxis.children.set(MARKER, marker);
await assert.rejects(
installMuiZip({archive, rootDirectory: root, metadata}),
/another map installer is already running/i,
);
// Nothing was published or activated; the foreign marker is untouched.
assert.equal(pyxis.children.has(metadata.packId), false);
assert.equal(pyxis.children.has('map-sets'), false);
assert.equal(pyxis.children.has('active-pack.0'), false);
assert.deepEqual([...marker.bytes], [...originalMarker]);
});
test('a stale install marker is reclaimed and the marker is released on success', async () => {
const archive = storedZip([['2/1/1.png', PNG]]);
const root = new MemoryDirectoryHandle('sd');
const pyxis = await root.getDirectoryHandle('pyxis-map', {create: true});
const marker = new MemoryFileHandle(MARKER, root.log);
marker.bytes = new TextEncoder().encode(`PYXI 1 cli-0 ${Date.now() - 24 * 60 * 60 * 1000}`);
pyxis.children.set(MARKER, marker);
const result = await installMuiZip({archive, rootDirectory: root, metadata});
assert.equal(result.tileCount, 1);
assert.equal(pyxis.children.has(MARKER), false);
});
test('activation aborts when a cross-producer commit lands before the record writes, then retry converges', async () => {
const archive = storedZip([['2/1/1.png', PNG]]);
const root = new MemoryDirectoryHandle('sd');
const pyxis = await root.getDirectoryHandle('pyxis-map', {create: true});
const mapSets = await pyxis.getDirectoryHandle('map-sets', {create: true});
const seed = (directory, name, bytes) => {
const file = new MemoryFileHandle(name, root.log);
file.bytes = bytes;
directory.children.set(name, file);
return file;
};
const gen1 = encodeActiveMapSet({generation: 1, mapSetId: metadata.mapSetId, attribution: metadata.attribution, packs: [{packId: 'old'}]});
seed(mapSets, `${metadata.mapSetId}.pmas`, gen1);
const slot1 = seed(pyxis, 'active-pack.0', gen1);
// Deterministic seam: active-pack.0 is read plainly (no create) twice per
// prepare pass -- once by readSelection, once by the raw slot snapshot --
// and the two prepare passes (installMapPack, then activateMapSet) are
// followed by the commit-time verifyActivationState re-read. That is the
// fifth plain read overall; inject the racer's commit right before it,
// i.e. between the snapshot derive and its revalidation.
let plainSlotReads = 0;
const realGetFileHandle = pyxis.getFileHandle.bind(pyxis);
pyxis.getFileHandle = async (name, options) => {
if (name === 'active-pack.0' && !(options && options.create)) plainSlotReads += 1;
if (name === 'active-pack.0' && !(options && options.create) && plainSlotReads === 5) {
const gen2 = encodeActiveMapSet({generation: 2, mapSetId: metadata.mapSetId, attribution: metadata.attribution, packs: [{packId: 'other'}, {packId: 'old'}]});
slot1.bytes = gen2;
seed(mapSets, `${metadata.mapSetId}.pmas`, gen2);
}
return realGetFileHandle(name, options);
};
try {
await assert.rejects(
installMuiZip({archive, rootDirectory: root, metadata}),
/changed during installation/i,
);
// The raced state is intact (no record was written over it) and our
// pack stays published under packs/ -- device-harmless until a record
// names it.
const packsDir = pyxis.children.get('packs');
assert.ok(packsDir, 'packs directory exists');
assert.equal(packsDir.children.has(metadata.packId), true);
const raced = await decodeActiveSelection(pyxis.children.get('active-pack.0').bytes);
assert.equal(raced.generation, 2);
assert.deepEqual(raced.packs.map(pack => pack.packId), ['other', 'old']);
const racedStyle = await decodeActiveSelection(mapSets.children.get(`${metadata.mapSetId}.pmas`).bytes);
assert.equal(racedStyle.generation, 2);
assert.equal(pyxis.children.has(MARKER), false, 'marker released even on abort');
} finally {
pyxis.getFileHandle = realGetFileHandle;
}
// The retry re-derives from the gen-2 state and converges to generation
// 3 in the free slot (the racer's gen-2 record keeps active-pack.0).
const retried = await installMuiZip({archive, rootDirectory: root, metadata});
assert.equal(retried.tileCount, 1);
const finalSlots = ['active-pack.0', 'active-pack.1']
.map(name => pyxis.children.get(name))
.filter(Boolean)
.map(handle => decodeActiveSelection(handle.bytes));
const newest = finalSlots.sort((left, right) => right.generation - left.generation)[0];
assert.equal(newest.generation, 3);
assert.deepEqual(newest.packs.map(pack => pack.packId), [metadata.packId, 'other', 'old']);
});
+228 -44
View File
@@ -32,6 +32,7 @@ import re
import stat
import struct
import sys
import time
from typing import Any, cast, Iterable
import zlib
@@ -1120,7 +1121,8 @@ STYLE_POLICIES: dict[str, dict[str, str]] = {
}
def activate_map_set(pyxis_fd: int, *, pack_id: str, map_set_id: str, attribution: str) -> tuple[str, list[str]]:
def activate_map_set(pyxis_fd: int, *, pack_id: str, map_set_id: str, attribution: str,
marker_token: str | None = None) -> tuple[str, list[str]]:
"""Publish the v3 active map-set record, mirroring the web flasher.
pyxis_fd is the open pyxis-map/ directory. Slot and style-record
@@ -1141,12 +1143,25 @@ def activate_map_set(pyxis_fd: int, *, pack_id: str, map_set_id: str, attributio
The final pack list is validated against the firmware's pack limit
before any byte is written, so a rejected activation cannot orphan a
published pack.
marker_token is the caller's cross-producer install-marker token (see
_acquire_install_marker). When given, the raw slot and style-record
bytes are captured when they are first read and re-read immediately
before the record writes: if another installer (the web flasher holds
a Web Locks name, which this process's flock cannot see) committed new
activation state -- or holds a fresh install marker that is not ours --
in the meantime, the publication is aborted before any byte is written.
The already-published pack is kept; a retry re-derives from the new
state and converges, and a pack that no record names is device-harmless
(MapTilePack only reads packs enumerated in the active selection).
Returns (slot_name, enabled_packs).
"""
slot_names = ("active-pack.0", "active-pack.1")
slots: list[dict[str, object] | None] = [None, None]
slot_raw: list[bytes | None] = [None, None]
for index, slot_name in enumerate(slot_names):
raw = _read_selection_at(pyxis_fd, slot_name)
slot_raw[index] = raw
if raw is not None:
slots[index] = decode_active_selection(raw)
if slots[0] is not None and slots[1] is not None \
@@ -1191,6 +1206,16 @@ def activate_map_set(pyxis_fd: int, *, pack_id: str, map_set_id: str, attributio
else:
target = slot_names[0] if cast(int, slots[0]["generation"]) <= \
cast(int, slots[1]["generation"]) else slot_names[1]
# Commit-time revalidation: the slot/style records were derived
# above; a concurrent cross-producer installer (the web flasher's
# Web Locks name is invisible to our flock) may have committed new
# activation state in the meantime. Re-reading the raw bytes here
# and aborting on any change guarantees this publication never
# overwrites a newer record with one derived from stale state.
# installed_raw is the style record's bytes as first read above.
_verify_activation_state_at(pyxis_fd, map_sets_fd, slot_raw=slot_raw,
style_raw=installed_raw, style_name=style_name,
marker_token=marker_token)
# Style record first, active slot second (the web flasher's order).
# The firmware's device-side style activation
# (MapStyleCatalog::activate) reads the style record as the source
@@ -1318,6 +1343,149 @@ def _install_lock_at(pyxis_fd: int):
os.close(descriptor)
# Cross-producer install marker.
#
# The web flasher holds a Web Locks name and this CLI holds an advisory
# flock: two unrelated locking namespaces, so each lock is invisible to
# the other producer. The on-disk marker below is the one coordination
# primitive both producers share -- the flasher writes the same file with
# the same token format (docs/flasher/js/map-installer.js). A marker is a
# small file containing: PYXI 1 <owner> <epoch_ms>.
_INSTALL_MARKER_NAME = ".pyxis-installing"
# A marker written within this window is a live installer. Installations
# longer than this are treated as abandoned (crashed builder, closed tab)
# and reclaimed. A future epoch (writer clock ahead of ours) counts as
# fresh: refusing is the safe direction.
_INSTALL_MARKER_TTL_SECONDS = 900
def _marker_token(owner: str) -> str:
return f"PYXI 1 {owner} {int(time.time() * 1000)}"
def _parse_marker(raw: bytes) -> tuple[str, int] | None:
"""Parse a marker token into (owner, epoch_ms); None if malformed."""
try:
text = raw.decode("ascii")
except UnicodeDecodeError:
return None
parts = text.strip().split(" ")
if len(parts) != 4 or parts[0] != "PYXI" or parts[1] != "1":
return None
if not parts[2] or len(parts[2]) > 64 or not parts[3].isdigit():
return None
return parts[2], int(parts[3])
def _marker_is_fresh(epoch_ms: int) -> bool:
now_ms = int(time.time() * 1000)
if epoch_ms > now_ms:
return True
return (now_ms - epoch_ms) / 1000.0 <= _INSTALL_MARKER_TTL_SECONDS
def _acquire_install_marker(pyxis_fd: int) -> str:
"""Claim the cross-producer install marker; return our token.
Protocol (shared with the web flasher):
* no marker -> create it;
* fresh foreign marker -> refuse: another installer is live;
* stale or malformed -> reclaim (unlink, then create).
After creation the file is read back and compared to our token: a
foreign content means we lost a simultaneous acquire race and we
abort. The marker is best-effort coordination -- the safety net is
the commit-time revalidation in activate_map_set, which refuses to
publish a record over activation state that moved since it was
derived.
"""
owner = f"cli-{os.getpid():x}"
token = _marker_token(owner)
raw = _read_selection_at(pyxis_fd, _INSTALL_MARKER_NAME)
if raw is not None:
parsed = _parse_marker(raw)
if parsed is not None and _marker_is_fresh(parsed[1]):
raise PackError("another map installer is already running on "
"this card; wait for it to finish and retry")
# Stale or malformed: reclaim. A malformed marker may be a live
# writer mid-write, but the read-back below fails loudly if so,
# and commit-time revalidation covers the rest.
_unlink_quietly_at(pyxis_fd, _INSTALL_MARKER_NAME)
try:
descriptor = os.open(_INSTALL_MARKER_NAME,
os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_CLOEXEC,
0o644, dir_fd=pyxis_fd)
except FileExistsError:
raise PackError("another map installer is already running on "
"this card; wait for it to finish and retry") from None
except OSError as exc:
raise PackError(f"cannot write the map-install marker: {exc}") from exc
try:
_write_all(descriptor, token.encode("ascii"), _INSTALL_MARKER_NAME)
os.fsync(descriptor)
finally:
os.close(descriptor)
actual = _read_selection_at(pyxis_fd, _INSTALL_MARKER_NAME)
if actual != token.encode("ascii"):
# Someone overwrote (or removed) our marker while we acquired it.
# Never delete a foreign marker: our claim is void, abort.
raise PackError("the map-install marker was contested during "
"acquisition; wait and retry")
_fsync_directory_fd(pyxis_fd)
return token
def _release_install_marker(pyxis_fd: int, token: str) -> None:
"""Remove our install marker -- and only ours.
If the marker content is not our own token (a later installer
reclaimed it after our install outlasted the TTL, or a crashed run
left a different claim), deleting it would drop that installer's
live claim. Leave it; the TTL reclaims it.
"""
raw = _read_selection_at(pyxis_fd, _INSTALL_MARKER_NAME)
if raw == token.encode("ascii"):
try:
os.unlink(_INSTALL_MARKER_NAME, dir_fd=pyxis_fd)
except OSError:
pass
def _verify_activation_state_at(pyxis_fd: int, map_sets_fd: int, *,
slot_raw: list[bytes | None],
style_raw: bytes | None, style_name: str,
marker_token: str | None) -> None:
"""Commit-time revalidation against a concurrent installer.
Re-reads the install marker and the raw activation records
immediately before the record writes. If a cross-producer installer
(web flasher or another builder) committed since the records were
derived -- or holds a fresh install marker that is not ours -- the
activation is aborted before any byte is written. The published pack
is kept; a retry re-derives from the new state and converges (the
pack is device-harmless while it is not named by any record: the
firmware only reads packs enumerated in the active selection).
"""
marker_raw = _read_selection_at(pyxis_fd, _INSTALL_MARKER_NAME)
if marker_raw is not None:
own = marker_token.encode("ascii") if marker_token is not None else None
parsed = _parse_marker(marker_raw)
if parsed is not None and _marker_is_fresh(parsed[1]) \
and marker_raw != own:
raise PackError("another map installer is running on this card; "
"wait for it to finish and retry")
for index, slot_name in enumerate(("active-pack.0", "active-pack.1")):
actual = _read_selection_at(pyxis_fd, slot_name)
if actual != slot_raw[index]:
raise PackError("active map-set records changed during "
"installation; wait for the other installer "
"to finish and retry")
actual_style = _read_selection_at(map_sets_fd, style_name)
if actual_style != style_raw:
raise PackError("installed style record changed during installation; "
"wait for the other installer to finish and retry")
def _remove_tree_at(parent_fd: int, name: str) -> None:
try:
descriptor = os.open(name, _DIRECTORY_FLAGS, dir_fd=parent_fd)
@@ -1512,52 +1680,68 @@ def build_map_pack(source_directory: Path, output_root: Path, *, pack_id: str, n
assert style is not None
# Hold the install lock from the preflight through activation:
# two concurrent builders could otherwise both pass the
# preflight and each clobber the other's records.
# preflight and each clobber the other's records. The cross-
# producer install marker (shared with the web flasher, which
# holds a Web Locks name this flock cannot see) is claimed for
# the same span and released on every exit path.
with _install_lock_at(pyxis_fd):
_preflight_activation(pyxis_fd, pack_id=pack_id, map_set_id=style,
attribution=attribution)
if not resumed:
temporary, stage_fd = _temporary_directory(packs_fd, pack_id)
copied = 0
for tile in tiles:
copy_tile(source_fd, tile, stage_fd, max_bytes - copied)
copied += tile.size
manifest_fd = os.open("manifest.pmp", os.O_WRONLY | os.O_CREAT | os.O_EXCL
| os.O_CLOEXEC, 0o644, dir_fd=stage_fd)
try:
_write_all(manifest_fd, manifest, "manifest.pmp")
os.fsync(manifest_fd)
finally:
os.close(manifest_fd)
_fsync_directory_fd(stage_fd)
_validate_pack_fd(stage_fd, target, max_bytes)
_verify_output_chain(output_root, output_fd, pyxis_fd, pyxis_identity,
packs_identity)
_rename_noreplace(packs_fd, temporary, pack_id, target)
committed = True
try:
_verify_output_chain(output_root, output_fd, pyxis_fd,
pyxis_identity, packs_identity)
except BaseException as exc:
# Claim the cross-producer install marker FIRST, before any
# staging or publication: a live web-flasher install (which
# holds a Web Locks name this flock cannot see) is announced
# by its marker, and refusing up front avoids the wasted
# tile copies and -- more importantly -- avoids publishing a
# pack that activation would then have to abandon. Stale or
# malformed markers (crashed builder, closed tab) are
# reclaimed here; ours is released on every exit path.
marker_token = _acquire_install_marker(pyxis_fd)
try:
_preflight_activation(pyxis_fd, pack_id=pack_id, map_set_id=style,
attribution=attribution)
if not resumed:
temporary, stage_fd = _temporary_directory(packs_fd, pack_id)
copied = 0
for tile in tiles:
copy_tile(source_fd, tile, stage_fd, max_bytes - copied)
copied += tile.size
manifest_fd = os.open("manifest.pmp", os.O_WRONLY | os.O_CREAT | os.O_EXCL
| os.O_CLOEXEC, 0o644, dir_fd=stage_fd)
try:
_remove_tree_at(packs_fd, pack_id)
_fsync_directory_fd(packs_fd)
committed = False
except BaseException as cleanup_error:
raise PublishedDurabilityError(target, OSError(
f"publication location changed and rollback failed: "
f"{cleanup_error}")) from exc
raise PackError("output directory changed during publication; "
"rolled back") from exc
try:
os.close(stage_fd)
except OSError as exc:
_write_all(manifest_fd, manifest, "manifest.pmp")
os.fsync(manifest_fd)
finally:
os.close(manifest_fd)
_fsync_directory_fd(stage_fd)
_validate_pack_fd(stage_fd, target, max_bytes)
_verify_output_chain(output_root, output_fd, pyxis_fd, pyxis_identity,
packs_identity)
_rename_noreplace(packs_fd, temporary, pack_id, target)
committed = True
try:
_verify_output_chain(output_root, output_fd, pyxis_fd,
pyxis_identity, packs_identity)
except BaseException as exc:
try:
_remove_tree_at(packs_fd, pack_id)
_fsync_directory_fd(packs_fd)
committed = False
except BaseException as cleanup_error:
raise PublishedDurabilityError(target, OSError(
f"publication location changed and rollback failed: "
f"{cleanup_error}")) from exc
raise PackError("output directory changed during publication; "
"rolled back") from exc
try:
os.close(stage_fd)
except OSError as exc:
stage_fd = -1
raise PublishedDurabilityError(target, exc) from exc
stage_fd = -1
raise PublishedDurabilityError(target, exc) from exc
stage_fd = -1
slot_name, _enabled = activate_map_set(pyxis_fd, pack_id=pack_id,
map_set_id=style,
attribution=attribution)
slot_name, _enabled = activate_map_set(pyxis_fd, pack_id=pack_id,
map_set_id=style,
attribution=attribution,
marker_token=marker_token)
finally:
_release_install_marker(pyxis_fd, marker_token)
else:
temporary, stage_fd = _temporary_directory(packs_fd, pack_id)
copied = 0