Fix what the last round of device fixes broke, and two dips it missed

This commit is contained in:
2026-08-21 08:50:00 -04:00
parent 4f0399b8ac
commit 5a58d33623
8 changed files with 517 additions and 26 deletions

View File

@@ -415,11 +415,13 @@ class ComputersPlugin(BasePlugin):
partmarkers = self._sync_partmarker(comp, pctype, machinenumber,
warnings, deviceid=deviceid)
# PC -> machine link from the reported machine number.
if partmarkers:
machinelinks = []
else:
# PC -> machine link from the reported machine number. Only a PC that
# is NOT a marker PC (None above) takes it: a marker PC reaches its
# operation through `partof` on the marker, never by claiming it.
if partmarkers is None:
machinelinks = self._sync_machine_link(comp, machinenumber, warnings)
else:
machinelinks = []
# Printer relationship sync (only when the payload carried printer data).
printerlinks = self._sync_printer_links(comp.asset, payload, warnings)
@@ -444,7 +446,10 @@ class ComputersPlugin(BasePlugin):
'measuringtoollinkcount': len(measuringtoollinks),
'accessprotocols': accessprotocols,
'machinelinks': machinelinks,
'partmarkers': partmarkers,
# `or []`: None is the internal "not a marker PC" signal and
# has no business in the response, where it would read as a
# different state from "a marker PC that linked nothing".
'partmarkers': partmarkers or [],
},
}
@@ -856,10 +861,21 @@ class ComputersPlugin(BasePlugin):
if not controls or not deviceasset or not pcasset:
return True
# EVERY collector-owned holder, not just ones under this label. On a
# CMM the instrument IS the reported bay, so the incumbent's link is the
# machine sync's row under MACHINE_LINK_ORIGIN. Filtering on our own
# label found no incumbent there, handed the challenger an active link,
# and left two live holders of one instrument - each invisible to the
# other because they were labelled differently.
#
# A row made by hand carries neither label and is deliberately NOT
# counted: a person's link is not the collector's to archive.
ourlabels = (label, MACHINE_LINK_ORIGIN, MEASURINGTOOL_LINK_ORIGIN,
PARTMARKER_LINK_ORIGIN)
others = AssetRelationship.query.filter(
AssetRelationship.targetassetid == deviceasset.assetid,
AssetRelationship.relationshiptypeid == controls.relationshiptypeid,
AssetRelationship.label == label,
AssetRelationship.label.in_(ourlabels),
AssetRelationship.isactive.is_(True),
AssetRelationship.sourceassetid != pcasset.assetid,
).all()
@@ -923,7 +939,16 @@ class ComputersPlugin(BasePlugin):
# Only a device that FILES UNDER an operation goes through here.
# A measuring tool is a subordinate device too, but it does not
# share a machine number, so it keeps the simpler path.
return []
#
# NONE, not []. None is the ONLY answer that means "not a marker PC,
# run the ordinary machine link". Every other exit below means "this
# IS a marker PC" - possibly one that linked nothing - and returning
# [] there let the caller fall through to the machine link, which
# for a marker PC points at the OPERATION. That is the direct claim
# this whole path exists to prevent: several markers serve one
# operation, so two marker PCs would contest a link that can only
# have one holder, while the warning said "not linked".
return None
pcasset = comp.asset
controls = RelationshipType.query.filter_by(
@@ -998,8 +1023,15 @@ class ComputersPlugin(BasePlugin):
self._ensure_device_rows(markerasset, spec, pcasset, controls,
label, warnings, active=mayhold)
elif reuse:
reuse.isactive = True
# Re-check incumbency HERE too, not only on the named path. A
# challenger recorded dormant keeps its row, so the cycle after its
# asset-id.txt goes missing found that row as `reuse` and flipped it
# active with no incumbent check and no warning - two live holders,
# arrived at by deleting a file. _sync_machine_link never had this
# hole because it re-checks unconditionally.
markerasset = db.session.get(Asset, reuse.targetassetid)
reuse.isactive = self._device_incumbents_yield(
markerasset, pcasset, label, warnings)
else:
coretype = AssetType.query.filter_by(
assettype=spec['assettype']).first()
@@ -1419,8 +1451,11 @@ class ComputersPlugin(BasePlugin):
tooltype.measuringtooltypeid
targetid = adopted.assetid
elif reuse:
reuse.isactive = True
# Same re-check as the marker path: a dormant challenger must not be
# promoted just because its enrollment file stopped being readable.
toolasset = db.session.get(Asset, reuse.targetassetid)
reuse.isactive = self._device_incumbents_yield(
toolasset, pcasset, MEASURINGTOOL_LINK_ORIGIN, warnings)
if toolasset and toolasset.measuringtool and tooltype:
toolasset.measuringtool.measuringtooltypeid = \
tooltype.measuringtooltypeid