mirror of
https://github.com/torlando-tech/pyxis.git
synced 2026-10-23 14:22:43 +00:00
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:
1 parent
370c5de629
commit
affd5566ab
4 files changed
+634
-65
No files matched your search
@@ -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)&¤t.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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user