Expose a non-owning wrapper factory for an existing cOAPI (#1) - #2
Conversation
Closes #1. EtabExtension.CLI must own the raw CSI lifecycle and the exact OS process identity itself — cHelper.CreateObject(path), a checked cOAPI.ApplicationStart(), a process census that proves which ETABS it owns, then a checked cSapModel.InitializeNewModel() — and only afterwards wants EtabSharp's model abstractions over that exact instance. No public path allowed that: the ETABSApplication constructor is internal, and Connect/ConnectToProcess/CreateNew each create the handle themselves. WrapExisting is pure wrapping. It validates its inputs, reads SapModel, and constructs the wrapper around the caller's exact object. It never creates, starts, attaches, enumerates processes, touches the ROT, hides, or exits. The constructor stays internal, and unlike the other factories this one throws rather than returning null, so the reason a wrap failed is not discarded. Also corrects the stale Dispose() documentation: the implementation releases COM references and has never called ApplicationExit. The comment claiming otherwise is what led a downstream design to ban Dispose outright, when the real rule is that Dispose is never a substitute for — nor to be called before — an authoritative ApplicationExit(false), and is the correct cleanup after it. Tests use a DispatchProxy recorder for cOAPI/cSapModel, so "touched nothing but SapModel" is proven rather than asserted by inspection. They are offline and start no ETABS process. The test project's ETABSv1 detection now mirrors the library's (lib\ first, then newest install) — it previously resolved only from an ETABS 22 install, so no test naming an ETABSv1 type could compile on a machine with ETABS 23 or 24. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tadoEng
left a comment
There was a problem hiding this comment.
Exact-head review at 95e2d026dfd158ac9886a8b461947dfb95ffc338.
The WrapExisting factory itself is accepted: pure wrap, exact cOAPI path, constructor remains internal, throwing semantics are appropriate, apiVersion == 0 is compatible with existing EtabSharp behavior, and the test-project ETABS 24→23→22 resolution + net8/net10 multi-targeting are acceptable. The DispatchProxy suite gives good negative evidence that the factory itself does not start/attach/exit or touch lifecycle members.
There is one blocking cleanup finding before merge, with two parts caused by the same Dispose contract:
-
The new XML wording overstates the ordering rule. Existing Mode A tests explicitly use
app.Dispose()to release an attached wrapper without callingApplicationExit, leaving the user's ETABS running. ThereforeDispose()cannot universally say it “must not be used before”ApplicationExit, nor that every caller must perform an exit. Correct distinction:- Dispose is always non-exiting COM/reference cleanup.
- For a non-owning/external attach, Dispose alone is the correct final wrapper cleanup and ETABS remains running.
- For a caller-owned session that the caller intends to terminate (the CLI Defect C case),
ApplicationExit(false)/ authoritative lifecycle resolution happens first, then Dispose releases wrapper-held references.
-
More importantly, current
Dispose()does not reliably fulfill the newly documented “releases COM references” contract after the high-level wrappers have been materialized._model.Valueis an EtabSharp-managedETABSModel, and_application.Valueis the managedETABSApplicationManager; neither is a COM RCW. YetDispose()callsMarshal.ReleaseComObjecton those managed wrappers first inside one outer try.Marshal.ReleaseComObjectis defined for COM RCWs and rejects non-COM objects, so the first such failure can jump to the catch and skip release of the actual raw_sapModeland_api. This matters directly to Defect C because the raw CSI lifecycle requires dropping the model/API references after the authoritative exit.
Required narrow correction in this PR:
- never call
Marshal.ReleaseComObjectonETABSModelorETABSApplicationManager; - release only actual raw COM references (
_sapModel,_api), with cleanup structured so one release failure cannot suppress the other; - preserve the invariant that Dispose never calls
ApplicationExit; - rewrite the XML docs with the owning-vs-attached distinction above;
- add/adjust offline tests that materialize both
app.Modelandapp.Application, then dispose, proving no lifecycle call is issued and double-dispose remains safe. The real COM-release mechanics remain a live/platform concern; do not start ETABS for this fix.
No need to revert the test-project multi-target/reference change. No version bump, package publish, merge, or CLI Defect C yet. Post the refreshed exact head + net8/net10 offline/build/pack evidence and stop again.
tadoEng
left a comment
There was a problem hiding this comment.
Additional note: I independently checked the .NET contract for Marshal.ReleaseComObject; it decrements an RCW and is only valid for a COM object. The blocking point above is therefore not stylistic — ETABSModel and ETABSApplicationManager are ordinary managed wrapper classes, so they must not be passed to ReleaseComObject.
Dispose released _model.Value and _application.Value through Marshal.ReleaseComObject, but those are ordinary managed EtabSharp wrappers — ETABSModel holds the cSapModel, ETABSApplicationManager holds the cOAPI. That API rejects a non-COM object, and because every release shared one try block, the first failure skipped the two releases that actually matter: _sapModel and _api. Only those two are released now, each in isolation so a failure on one cannot prevent the other. Dispose stays completely non-exiting. The XML wording is corrected too. Saying Dispose "must not be used before" ApplicationExit contradicted the library's own Mode A contract, where an attached session disposes without an exit and the user's ETABS is meant to stay running. The rule is per-mode: attached sessions dispose on their own; a caller-owned session being shut down resolves the authoritative ApplicationExit(false) and process exit first, then disposes. The offline test now materializes both lazy wrappers before disposing and asserts two independent release attempts, no lifecycle call, and a safe double dispose. Restoring the old single-try body fails that test, so it detects the defect rather than merely passing over it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Blocker closed — refreshed exact head
|
Lead exact-head acceptance —
|
Closes #1.
Exact reviewed head:
2fdc5d70d37cddc1905f1825327719482568efcd, based onbaa07045b145cdbcc7fb25314211e7455718428e.What this adds
WrapExistingis deliberately a pure wrapper over an already-created, already-started caller-suppliedcOAPI. The only member it reads isSapModel. It does not create/start/attach through PID or ROT, enumerate/select ETABS processes, hide/unhide, or exit ETABS.ETABSApplication's constructor remains internal.It throws on invalid caller input rather than returning null.
apiVersion == 0is accepted because EtabSharp's existing version discovery uses 0 when the OAPI version cannot be read.Lifecycle / Dispose contract
ETABSApplication.Dispose()is non-exiting reference cleanup; it never callsApplicationExit.Dispose()alone is correct wrapper cleanup and the user's ETABS remains running.ApplicationExit(false)/ process exit first, then disposes the wrapper.The stale XML documentation claiming Dispose exits ETABS was corrected.
A pre-existing COM-release defect found during review was also fixed: managed EtabSharp wrappers (
ETABSModel,ETABSApplicationManager) are no longer passed toMarshal.ReleaseComObject. Only the actual raw_sapModeland_apireferences are attempted, each independently so failure releasing one cannot suppress the other.Offline tests
test/EtabSharp.Test/wrap_existing_test.cscontains 14[Trait("Category", "Offline")]tests usingDispatchProxyrecorders. They cover:WrapExisting;SapModelfails rather than starting ETABS;ApplicationExit;app.Modelandapp.Applicationbefore Dispose still results in two independent raw COM-release attempts;Mutation check: restoring the former single-try Dispose body makes the materialized-wrapper cleanup test fail (
Expected: 2, Actual: 0), proving the test detects the defect.Evidence at exact head
baa07045exactly; no suppressions added;dotnet packsucceeds withlib/net8.0andlib/net10.0;ETABSv1.dllis not bundled in the package;The test project now resolves ETABSv1 consistently with the library (
lib\\override, then installed ETABS 24 → 23 → 22) and targets both net8.0 and net10.0.No live ETABS operation is part of this PR.