app
Fusion - the Fusion 360 Bridge
Public Made by Adomby adom
Drive Autodesk Fusion 360 from the cloud via Adom Bridge: component libraries, IPC package generation, board layout, exports (STEP/Gerbers/BOM/CPL), fast APS cloud search, and parametric modeling.
master
Drew Owens
The mechanical layer on 1.12.8 with the restart fixes (1.13.0-dev.1, add-in 1.0.23)
2ef0220
14d ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
"""The bridge exe's mechanical protocol in Python, for offline add-in tests.
Mirrors rust/bridge/src/mech.rs step for step (validate, admit, dispatch with a claim, settle from
the add-in's answer or its result file) on top of tests/mech_reference, the dev.9 admission code the
Rust port is pinned to. The add-in under test only sees what the exe would send it.
"""
import copy
import json
import math
import os
import uuid
from mech_reference.journal import (Journal, JournalError, MAX_DATABASE_BYTES, MAX_RECORDS, MAX_RECORD_BYTES,
MAX_REQUEST_BYTES, STORAGE_VERSION, canonical)
from mech_reference.operation_history import OperationHistory
UNITS = {"length": "mm", "angle": "deg", "mass": "kg", "area": "mm2", "volume": "mm3"}
DISPATCH_FIELDS = ("success", "dispatchId", "nativeExecutionStarted", "observedDocument", "result")
class MechanicalError(Exception):
def __init__(self, code, message, **details):
super().__init__(message)
self.code, self.details = code, details
def require(condition, code, message, **details):
if not condition:
raise MechanicalError(code, message, **details)
def validate(value, schema, path="args"):
"""dev.9 __init__.validate, verbatim."""
if "oneOf" in schema:
variants = schema["oneOf"]
if isinstance(value, dict):
for discriminator in ("type", "action"):
if discriminator in value:
matching = [variant for variant in variants if value[discriminator] in
variant.get("properties", {}).get(discriminator, {}).get("enum", [])]
if len(matching) == 1:
validate(value, matching[0], path)
variants = []
break
if variants:
successes = 0
for variant in variants:
try:
validate(value, variant, path)
successes += 1
except MechanicalError:
pass
require(successes == 1, "invalid_input", path + " must match exactly one supported schema variant")
kinds = {"object": dict, "string": str, "boolean": bool, "number": (int, float), "integer": int, "array": list}
kind = schema.get("type")
if kind:
require(isinstance(value, kinds[kind]) and not (kind in ("number", "integer") and isinstance(value, bool)),
"invalid_input", path + " must be " + kind)
if kind in ("number", "integer"):
require(math.isfinite(value), "invalid_input", path + " must be finite")
if "exclusiveMinimum" in schema:
require(value > schema["exclusiveMinimum"], "invalid_input", path + " is too small")
if "minimum" in schema:
require(value >= schema["minimum"], "invalid_input", path + " is too small")
if "maximum" in schema:
require(value <= schema["maximum"], "invalid_input", path + " is too large")
if kind == "array":
require(schema.get("minItems", 0) <= len(value) <= schema.get("maxItems", 10000), "invalid_input", path + " has an invalid item count")
for index, item in enumerate(value):
validate(item, schema.get("items", {}), path + "[" + str(index) + "]")
if "enum" in schema:
require(value in schema["enum"], "invalid_input", path + " has an unsupported value")
if kind == "object":
require(len(value) >= schema.get("minProperties", 0), "invalid_input", path + " requires at least one property")
for key in schema.get("required", []):
require(key in value, "invalid_input", path + "." + key + " is required")
props = schema.get("properties", {})
if schema.get("additionalProperties") is False:
require(not (set(value) - set(props)), "invalid_input", path + " has unknown fields")
for key in value.keys() & props.keys():
validate(value[key], props[key], path + "." + key)
if isinstance(schema.get("additionalProperties"), dict):
for key in value.keys() - props.keys():
validate(value[key], schema["additionalProperties"], path + "." + key)
def prepare_request(args, schema, writes, session_id):
"""dev.9 __init__.prepare_request, with the schema, write set and current session passed in."""
require(isinstance(args, dict), "invalid_input", "Arguments must be an object.")
args = {key: value for key, value in args.items() if key not in ("caller", "_callerBudgetMs")}
op = args.get("op")
require(isinstance(op, str) and op in schema, "unsupported_operation", "Discover supported operations with fusion_mech_capabilities.")
validate(args, schema[op])
allowed = set(schema[op]["properties"]) | {"op", "sessionId", "operationId", "target"}
require(not (set(args) - allowed), "invalid_input", "Unknown operation arguments: " + ", ".join(sorted(set(args) - allowed)))
write = op in writes or (op == "health" and args.get("recompute", False))
if write:
require(args.get("sessionId") == session_id, "session_mismatch", "Old-session history is readable, but native execution requires the current add-in session.")
require(isinstance(args.get("operationId"), str) and 1 <= len(args["operationId"]) <= 128,
"operation_id_required", "Every write needs a unique operationId; retries must preserve the exact request.")
return json.loads(canonical(args, MAX_REQUEST_BYTES)), write
def capabilities(schema, session_id, transactions_capability):
"""dev.9 __init__.capabilities, with the schema, session and probe state passed in."""
return {"success": True, "sessionId": session_id, "schemaVersion": 1, "units": UNITS,
"operations": schema, "transactions": {"genericAtomicBatch": False, "typedModelOperations": transactions_capability,
"parameterRecovery": "compensating expression restore with semantic readback",
"newDesignRecovery": "discard only the new unsaved document",
"externalEffects": "exports publish atomically; model recovery does not undo files"},
"idempotency": {"scope": "durable history keyed by add-in session and operation ID", "capacity": MAX_RECORDS,
"storageVersion": STORAGE_VERSION, "databaseByteLimit": MAX_DATABASE_BYTES,
"recordByteLimit": MAX_RECORD_BYTES, "admission": "FULL SQLite commit before native dispatch",
"restartBehavior": "old-session history is readable; native execution never replays across sessions"}}
class Exe:
"""One exe over one journal directory, dispatching to the add-in package `m`."""
def __init__(self, m, folder, source_version="offline-test"):
self.m, self.folder = m, folder
self.history = OperationHistory(m.SESSION_ID, source_version, Journal(folder))
def store(self):
return self.history.store()
def admit(self, request, dispatch_id):
return self.history.admit(request, dispatch_id)
def status(self, args):
return self.history.status(args)
def start(self, descriptor, request, document):
return self.history.start(descriptor, request, document)
def _dispatch_file(self, dispatch_id, suffix):
return os.path.join(self.folder, "dispatch", dispatch_id + suffix)
def call(self, app, args):
m = self.m
envelope = {k: args.pop(k) for k in ("foreground", "foregroundReason") if isinstance(args, dict) and k in args}
try:
frozen, write = prepare_request(copy.deepcopy(args), m.SCHEMA, m.WRITES, m.SESSION_ID)
except (MechanicalError, JournalError) as error:
return {"success": False, "sessionId": m.SESSION_ID, "errorCode": error.code, "error": str(error), **getattr(error, "details", {})}
if not write:
return m.handle_mechanical(app, {**copy.deepcopy(frozen), **envelope})
dispatch_id = uuid.uuid4().hex
descriptor, reply = self.history.admit(frozen, dispatch_id)
if reply is not None:
return reply
return self.dispatch(app, frozen, descriptor)
def dispatch(self, app, frozen, descriptor):
answer = self.m.handle_mechanical(app, {**copy.deepcopy(frozen), "_dispatch": {"dispatchId": descriptor["dispatchId"]}})
return self.settle(descriptor, frozen, answer)
def settle(self, descriptor, frozen, answer):
dispatch_id = descriptor["dispatchId"]
answered = isinstance(answer, dict) and answer.get("dispatchId") == dispatch_id
if answered and answer.get("nativeExecutionStarted") is True:
return self._started(descriptor, frozen, answer.get("observedDocument"), answer)
claim = self._dispatch_file(dispatch_id, ".claim")
os.makedirs(os.path.dirname(claim), exist_ok=True)
try:
os.close(os.open(claim, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600))
except FileExistsError:
with open(claim, encoding="utf-8") as stream:
text = stream.read()
state = json.loads(text) if text else {}
if state.get("state") != "expired":
return self._started(descriptor, frozen, state.get("observedDocument"), answer)
result = answer["result"] if answered else answer
return self._envelope(self.history.not_executed(descriptor, result), answer if answered else {})
def _started(self, descriptor, frozen, document, answer):
pending = self.history.start(descriptor, frozen, document)
if pending is not None:
return self._envelope(pending, answer)
path = self._dispatch_file(descriptor["dispatchId"], ".result.json")
result = None
if os.path.exists(path):
with open(path, encoding="ascii") as stream:
result = json.load(stream)
elif isinstance(answer, dict) and answer.get("dispatchId") == descriptor["dispatchId"]:
result = answer.get("result")
if result is None:
record = self.store().lookup(descriptor["sessionId"], descriptor["operationId"])
return self._envelope(self.history.pending_reply(record), answer)
return self._envelope(self.history.finish(descriptor, result), answer)
@staticmethod
def _envelope(reply, answer):
if isinstance(reply, dict) and isinstance(answer, dict):
for key, value in answer.items():
if key not in DISPATCH_FIELDS and key not in reply:
reply[key] = value
return reply