Implement engine-bridge v2 plugin masquerade (#10)
This commit is contained in:
75
docs/contracts/engine-bridge-v2.md
Normal file
75
docs/contracts/engine-bridge-v2.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Sprint Contract — engine-bridge v2 (MEF plugin masquerade)
|
||||
|
||||
**Owner:** Generator
|
||||
**Depends on:** engine-bridge v1 (완료)
|
||||
**Issue:** #10
|
||||
|
||||
## Goal
|
||||
|
||||
`engine-bridge` v1 probe design 문서가 권고한 **MEF plugin masquerade** 경로를 구현한다. recordingtest 전용 플러그인을 빌드해 `EG-BIM Modeler/Plugins/`에 drop-in하면, SUT가 로드할 때 플러그인이 로컬 HTTP 서버를 띄워 HmEG 상태(선택/카메라/씬/렌더)를 JSON으로 노출한다. recordingtest-side 클라이언트는 HTTP로 상태를 조회한다.
|
||||
|
||||
PoC 범위: 플러그인 + 클라이언트 빌드 + 엔드투엔드 HTTP 테스트 (가짜 엔진 상태 주입). **SUT 실제 실행은 수동 smoke test 단계에서.**
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- [ ] `src/Recordingtest.EgPlugin/` — MEF plugin dll
|
||||
- Target: `net8.0-windows`
|
||||
- `Editor03.PluginInterface.dll`을 `HintPath`로 참조 (Private=false, SUT의 정본 복사 금지)
|
||||
- `Editor03.PluginInterface.dll`의 **실제 계약을 MetadataLoadContext로 분석**해서 구현할 최소 interface/attribute를 확인 (Generator가 discovery 수행)
|
||||
- 플러그인 로드 시점에 `HttpListener`로 `http://localhost:{port}/` 시작 (기본 포트 `38080`, 환경변수 `RECORDINGTEST_BRIDGE_PORT`로 override)
|
||||
- 엔드포인트: `/selection`, `/camera`, `/scene`, `/render`, `/health`
|
||||
- 각 엔드포인트는 `IEngineStateProvider` 인터페이스로부터 JSON을 반환
|
||||
- 기본 `ReflectionEngineStateProvider`는 HmEG 내부 타입을 리플렉션으로 찾아 상태를 구성 (실패해도 예외 삼킴 — `{error: "..."}` 반환)
|
||||
- 플러그인 unload 시 HttpListener 정리
|
||||
- [ ] `src/Recordingtest.EngineBridge.Client/` — HTTP 클라이언트 라이브러리
|
||||
- `HmEgHttpSnapshot : IEngineSnapshot` 구현 (v1 인터페이스 재사용)
|
||||
- `HttpClient`로 엔드포인트 호출, 타임아웃 기본 2초
|
||||
- 각 속성은 on-demand HTTP GET (caching 없음 v2)
|
||||
- `IsRenderComplete`는 `/render` 응답의 `complete` 필드
|
||||
- [ ] `tests/Recordingtest.EngineBridge.IntegrationTests/` — xUnit
|
||||
- **fake HTTP 서버**: `HttpListener`를 테스트 안에서 띄우고 고정 JSON 응답
|
||||
- 테스트 ≥ 6:
|
||||
1. `Client_SelectionEndpoint_ReturnsIds`
|
||||
2. `Client_CameraEndpoint_ReturnsCameraState`
|
||||
3. `Client_SceneEndpoint_ReturnsSceneSummary`
|
||||
4. `Client_RenderEndpoint_ReturnsIsComplete`
|
||||
5. `Client_HealthEndpoint_ReturnsOk`
|
||||
6. `Client_Timeout_ThrowsOrReturnsError` (2초 이내 미응답 시)
|
||||
- [ ] `src/Recordingtest.EgPlugin` 단위 테스트 ≥ 3 (플러그인 로직)
|
||||
- 테스트에서 **실제 plugin dll을 SUT에 주입하지 않음**. 순수 로직만.
|
||||
- `StateRouter_SerializesSelection_ToJson`
|
||||
- `StateRouter_WithFaultyProvider_ReturnsErrorPayload`
|
||||
- `PortResolver_PrefersEnvVar` (`RECORDINGTEST_BRIDGE_PORT`)
|
||||
- [ ] `docs/guides/engine-bridge-deploy.md` — 수동 배포 가이드 (SUT Plugins/ 폴더에 dll 복사, 환경변수 설정, 검증 절차)
|
||||
- [ ] `dotnet build` + `dotnet test` 전부 green
|
||||
- [ ] 플러그인이 SUT 자체 파일을 건드리지 않음 (guard hook 준수)
|
||||
|
||||
## Out of scope
|
||||
|
||||
- SUT 실제 실행 (수동 smoke test)
|
||||
- 인증/암호화 (PoC는 localhost only)
|
||||
- 멀티클라이언트 / 동시성 (단일 HttpListener)
|
||||
- HmEG 리플렉션 매핑 완성도 — v2는 skeleton + error fallback 중심, 진짜 매핑은 smoke test 후 조정
|
||||
|
||||
## Interfaces
|
||||
|
||||
- **Inputs:** SUT가 plugin을 로드할 때의 MEF 계약
|
||||
- **Outputs:** `http://localhost:<port>/{selection|camera|scene|render|health}` JSON
|
||||
- **Side effects:** HttpListener 포트 점유 (플러그인 생명주기 내)
|
||||
|
||||
## Evaluation plan
|
||||
|
||||
1. `dotnet build recordingtest.sln` green
|
||||
2. `dotnet test tests/Recordingtest.EngineBridge.IntegrationTests` — 6 pass
|
||||
3. `dotnet test tests/Recordingtest.EgPlugin.Tests` — 3 pass
|
||||
4. `Recordingtest.EgPlugin.dll` 출력 확인, `Editor03.PluginInterface`에 대한 HintPath/Private=false 확인 (csproj 리뷰)
|
||||
5. Generator가 Editor03.PluginInterface.dll 메타데이터에서 발견한 실제 인터페이스 이름을 history에 기록했는지 확인
|
||||
6. `docs/guides/engine-bridge-deploy.md` 존재 + 배포 단계(복사·환경변수·검증) 기술
|
||||
7. SUT 폴더에 쓰기 흔적 없음 (grep)
|
||||
8. Plugin 코드가 `HttpListener` 예외 상황(port 충돌, stop) 처리
|
||||
|
||||
## Risks
|
||||
|
||||
- `Editor03.PluginInterface`가 WPF dependency를 가지면 net8.0-windows로 충분하지 않을 수 있음 — 필요 시 TFM 조정
|
||||
- 플러그인 dll이 SUT와 같은 디렉터리의 다른 dll 버전과 충돌 가능 — Private=false 필수
|
||||
- HmEG 리플렉션 실제 매핑은 SUT 런타임에서만 검증 가능 → v2는 error fallback 철저
|
||||
97
docs/guides/engine-bridge-deploy.md
Normal file
97
docs/guides/engine-bridge-deploy.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# engine-bridge v2 Deployment Guide
|
||||
|
||||
This guide explains how to deploy `Recordingtest.EgPlugin.dll` into the SUT
|
||||
(`EG-BIM Modeler`) so the recordingtest harness can read live HmEG state over
|
||||
HTTP. Issue #10.
|
||||
|
||||
## 1. Build
|
||||
|
||||
```
|
||||
dotnet publish src/Recordingtest.EgPlugin -c Release
|
||||
```
|
||||
|
||||
Output: `src/Recordingtest.EgPlugin/bin/Release/net8.0-windows/publish/`
|
||||
- `Recordingtest.EgPlugin.dll`
|
||||
- `Recordingtest.EgPlugin.deps.json`
|
||||
- `Recordingtest.EgPlugin.pdb`
|
||||
|
||||
`Editor03.PluginInterface.dll` and `HmEG.dll` are referenced with
|
||||
`<Private>false</Private>`, so the plugin output **does not** include copies
|
||||
of the SUT contracts. The plugin will bind to whatever the SUT loads at
|
||||
runtime.
|
||||
|
||||
## 2. Copy into SUT Plugins folder
|
||||
|
||||
> **Important:** writing into `EG-BIM Modeler/` is normally blocked by the
|
||||
> repo guard hook. Ask the operator before performing the copy step. The
|
||||
> copy is intentionally a manual operation.
|
||||
|
||||
Create a per-plugin folder under `EG-BIM Modeler/Plugins/` and copy the
|
||||
publish output:
|
||||
|
||||
```
|
||||
mkdir "EG-BIM Modeler\Plugins\Recordingtest.EgPlugin"
|
||||
copy src\Recordingtest.EgPlugin\bin\Release\net8.0-windows\publish\Recordingtest.EgPlugin.dll "EG-BIM Modeler\Plugins\Recordingtest.EgPlugin\"
|
||||
copy src\Recordingtest.EgPlugin\bin\Release\net8.0-windows\publish\Recordingtest.EgPlugin.deps.json "EG-BIM Modeler\Plugins\Recordingtest.EgPlugin\"
|
||||
```
|
||||
|
||||
`runtimeconfig.json` is not produced for class library projects; the SUT
|
||||
hosts the CLR. If a future change makes the plugin executable, also copy
|
||||
`Recordingtest.EgPlugin.runtimeconfig.json`.
|
||||
|
||||
## 3. Configure environment (optional)
|
||||
|
||||
The plugin listens on `http://localhost:38080/` by default. Override with:
|
||||
|
||||
```
|
||||
set RECORDINGTEST_BRIDGE_PORT=38090
|
||||
```
|
||||
|
||||
The variable is read once at plugin construction.
|
||||
|
||||
## 4. Launch SUT
|
||||
|
||||
Start `EG-BIM Modeler.exe` normally. The SUT's `HmEG.PluginLoader` walks
|
||||
`Plugins/` at startup and loads any DLL whose type derives from
|
||||
`HmEG.IPlugin` (our plugin inherits `Editor.PluginInterface.EditorPlugin`,
|
||||
which implements `IPlugin`).
|
||||
|
||||
## 5. Verify
|
||||
|
||||
```
|
||||
curl http://localhost:38080/health
|
||||
```
|
||||
|
||||
Expected:
|
||||
|
||||
```
|
||||
{"status":"ok","port":38080}
|
||||
```
|
||||
|
||||
Other endpoints:
|
||||
|
||||
- `GET /selection` -> `{"selected_ids":[...]}`
|
||||
- `GET /camera` -> `{"eye":[..],"target":[..],"up":[..],"fov":n}`
|
||||
- `GET /scene` -> `{"object_count":n,"document_path":"..."}`
|
||||
- `GET /render` -> `{"complete":true|false}`
|
||||
|
||||
The recordingtest client (`HmEgHttpSnapshot`) is the supported consumer.
|
||||
|
||||
## 6. Troubleshooting
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---|---|---|
|
||||
| `curl` connection refused | port already in use OR plugin failed to load | check SUT log under `EG-BIM Modeler/hmlogs/`, set a different `RECORDINGTEST_BRIDGE_PORT` |
|
||||
| `Could not load file or assembly Editor03.PluginInterface` | Wrong contract version dropped next to plugin | delete any local copy of `Editor03.PluginInterface.dll` from the plugin folder; the SUT must resolve it from its own folder |
|
||||
| Plugin loaded but `/health` 404 | SUT started a different `Plugins/Recordingtest.EgPlugin/` build | clean the folder and re-copy publish output |
|
||||
| `HttpListener` access denied (Windows) | URL ACL not registered | run SUT elevated once, or `netsh http add urlacl url=http://localhost:38080/ user=Everyone` |
|
||||
|
||||
SUT log location: `EG-BIM Modeler/hmlogs/` (Serilog rolling files).
|
||||
|
||||
## 7. Uninstall
|
||||
|
||||
```
|
||||
rmdir /s /q "EG-BIM Modeler\Plugins\Recordingtest.EgPlugin"
|
||||
```
|
||||
|
||||
No registry, no services, no env vars beyond optional `RECORDINGTEST_BRIDGE_PORT`.
|
||||
78
docs/history/2026-04-07_이슈10-engine-bridge-v2-generator.md
Normal file
78
docs/history/2026-04-07_이슈10-engine-bridge-v2-generator.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# 2026-04-07 — Issue #10 engine-bridge v2 Generator
|
||||
|
||||
- 역할: Generator
|
||||
- 계약: `docs/contracts/engine-bridge-v2.md`
|
||||
- 소요 시간: 약 35분 (단일 세션)
|
||||
- Context 사용량: 약 95k tokens (대용량 sln/.cs 디렉터리 스캔 + MetadataLoadContext 출력)
|
||||
|
||||
## Editor03 / HmEG 디스커버리 결과
|
||||
|
||||
`MetadataLoadContext`로 `EG-BIM Modeler/Editor03.PluginInterface.dll`,
|
||||
`Editor07.WidgetPluginInterface.dll`, `HmEG.dll`, `Plugins/EgBoxPlugin/EgBoxPlugin.dll`을
|
||||
메타데이터 전용으로 열어 다음을 확인했다:
|
||||
|
||||
- **실제 plugin 컨트랙트는 `HmEG.IPlugin`** (HmEG.dll). Members:
|
||||
`string Name { get; }`, `EGViewport View { get; set; }`,
|
||||
`bool RethrowException { get; set; }`, `object Run(object[])`.
|
||||
- **MEF Export 어트리뷰트는 사용되지 않는다.** 로딩은 `HmEG.PluginLoader`
|
||||
(`LoadProjectPlugins`/`LoadPlugin(path,name)`)가 직접 수행한다.
|
||||
`EgBoxPlugin.EgBoxPlugin` 샘플도 `[Export]` 없이 단순히
|
||||
`Editor.PluginInterface.EditorPlugin`을 상속한다.
|
||||
- **`Editor.PluginInterface.EditorPlugin`** (Editor03.PluginInterface.dll)
|
||||
은 `HmEG.IPlugin`을 구현하는 추상 base다.
|
||||
abstract 멤버: `Name { get; }`, `Description { get; }`,
|
||||
`protected void Initialize()`. 그 외 `AppManager`, `RootSpace`,
|
||||
`ViewportManager`, `View`, `AddModelToRootSpace(...)` 등의 헬퍼를 노출한다.
|
||||
- **`Editor07.WidgetPluginInterface`** 는 위젯 전용(`WidzetPlugin`,
|
||||
`HmEG_DebugWidzetPlugin`)이며 v2 범위 밖이라 미사용.
|
||||
|
||||
→ 결론: `HmEgBridgePlugin : Editor.PluginInterface.EditorPlugin` 으로
|
||||
구현. `Name`/`Description` override + `protected override Initialize()`,
|
||||
HTTP listener는 생성자에서 안전하게 부팅 (`Initialize`가 호출되지 않더라도
|
||||
listener는 살아있음).
|
||||
|
||||
### 디스커버리 시 주의사항
|
||||
|
||||
- MetadataLoadContext에 `Microsoft.NETCore.App` 런타임 디렉터리만 넣으면
|
||||
`WindowsBase`의 `System.Windows.Markup.InternalTypeHelper`를 못 찾아
|
||||
`TypeLoadException`이 난다. **WindowsDesktop ref pack
|
||||
(`packs/Microsoft.WindowsDesktop.App.Ref/8.0.22/ref/net8.0`)** 도 함께
|
||||
resolver에 등록해야 한다. 동일 파일명은 dedupe 필요.
|
||||
- 같은 이유로 `Editor03.PluginInterface`는 `<UseWPF>true</UseWPF>`인
|
||||
net8.0-windows 프로젝트에서만 빌드된다.
|
||||
|
||||
## 산출물
|
||||
|
||||
- `src/Recordingtest.EgPlugin/` (PortResolver, IEngineStateProvider,
|
||||
Null/ReflectionEngineStateProvider, StateRouter, BridgeHttpServer,
|
||||
HmEgBridgePlugin)
|
||||
- `src/Recordingtest.EngineBridge.Client/` (HmEgHttpSnapshot,
|
||||
EngineBridgeException)
|
||||
- `tests/Recordingtest.EngineBridge.IntegrationTests/` (FakeBridgeServer +
|
||||
6 xUnit tests)
|
||||
- `tests/Recordingtest.EgPlugin.Tests/` (5 xUnit tests for StateRouter +
|
||||
PortResolver)
|
||||
- `docs/guides/engine-bridge-deploy.md`
|
||||
|
||||
## 빌드 / 테스트 결과
|
||||
|
||||
- `dotnet build recordingtest.sln` → green (warning 0, error 0)
|
||||
- `dotnet test tests/Recordingtest.EngineBridge.IntegrationTests` → 6/6 통과
|
||||
- `dotnet test tests/Recordingtest.EgPlugin.Tests` → 5/5 통과
|
||||
- 신규 프로젝트 4개를 `recordingtest.sln`에 추가
|
||||
- `EG-BIM Modeler/` 폴더에는 일체 쓰지 않음 (가드 훅 준수). 배포는 가이드
|
||||
문서로만 기술.
|
||||
|
||||
## 리스크 / 후속
|
||||
|
||||
- `ReflectionEngineStateProvider`는 v2 skeleton: 모든 메서드가 안전한
|
||||
default를 반환한다. **HmEG 내부 타입(특히 `HmEGAppManager`,
|
||||
`EGViewport`, 선택/카메라 manager)에 대한 진짜 reflection 매핑은 v3에서
|
||||
smoke test 후 확정해야 한다.** 후보 멤버는
|
||||
`docs/engine-catalog/hmeg-candidates.json` 참고.
|
||||
- `HttpListener` urlacl이 등록 안 된 환경에서는 첫 실행에 관리자 권한 또는
|
||||
`netsh http add urlacl` 필요 — 가이드에 명시.
|
||||
- `Editor.PluginInterface.EditorPlugin.Initialize()`가 protected이고
|
||||
HmEG.PluginLoader가 어느 시점에 호출하는지는 메타데이터로 확인 불가
|
||||
(런타임 동작). 그래서 listener를 생성자에서 부팅했다. PluginLoader가
|
||||
생성자 호출만으로 충분한지는 smoke test에서 확인 필요.
|
||||
Reference in New Issue
Block a user