fix(bridge): v0.3.5 — inline script + deterministic port + auto-checksum
- vscode-file:// refuses custom .js files → inline script into HTML - Random port → deterministic port from project name hash (gravity_control=34332) - Hardcoded port in renderer script for immediate discovery - Auto-update product.json SHA256 checksums after HTML modification - Bump version 0.2.0 → 0.3.5
This commit is contained in:
101
.agents/references/known-issues.md
Normal file
101
.agents/references/known-issues.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
# Known Issues & Lessons Learned
|
||||||
|
|
||||||
|
> **이 파일은 SSOT(Single Source of Truth)입니다.**
|
||||||
|
> 디버깅이나 구현 전에 **반드시** 이 파일을 확인하세요.
|
||||||
|
> 세션 종료 시 새로 발견된 이슈를 이 파일에 추가합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 포맷
|
||||||
|
|
||||||
|
각 항목은 아래 형식을 따릅니다:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### [날짜] [키워드] — 한줄 요약
|
||||||
|
- **증상**: 무엇이 잘못되었는가
|
||||||
|
- **원인**: 근본 원인
|
||||||
|
- **해결**: 올바른 해결 방법
|
||||||
|
- **주의**: 재발 방지를 위한 교훈
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 공통 이슈
|
||||||
|
|
||||||
|
### [2026-03-08] PowerShell curl — Invoke-WebRequest 충돌
|
||||||
|
- **증상**: `curl` 명령이 예상과 다른 응답 형식을 반환
|
||||||
|
- **원인**: PowerShell에서 `curl`은 `Invoke-WebRequest`의 별칭
|
||||||
|
- **해결**: **`curl.exe`**를 명시적으로 사용
|
||||||
|
- **주의**: HTTP 관련 모든 명령에서 `curl.exe` 사용 필수
|
||||||
|
|
||||||
|
### [2026-03-08] PowerShell npm — 실행 정책 오류
|
||||||
|
- **증상**: `npm run` 명령이 `실행 정책` 관련 오류로 실패
|
||||||
|
- **원인**: PowerShell 스크립트 실행 정책이 제한적으로 설정됨
|
||||||
|
- **해결**: `cmd /c npm run dev` 형식으로 cmd를 통해 실행
|
||||||
|
- **주의**: npm 관련 명령은 항상 `cmd /c` 접두어 사용 권장
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 프로젝트별 이슈
|
||||||
|
|
||||||
|
> 아래에 프로젝트 특화 이슈를 추가하세요.
|
||||||
|
|
||||||
|
### [2026-03-08] Antigravity Renderer Injection — Electron 캐시 차단
|
||||||
|
- **증상**: workbench.html, workbench-jetski-agent.html에 `<script>` 태그 추가 후 리로드해도 실행되지 않음
|
||||||
|
- **원인**: Electron의 V8 코드 캐시가 수정된 HTML을 무시하고 캐시된 버전을 서빙
|
||||||
|
- **해결**: 렌더러 인젝션 방식 **포기**. Extension Host에서 RPC 폴링 방식으로 전환
|
||||||
|
- **주의**: Antigravity는 `workbench-jetski-agent.html`을 사용 (Jetski = 내부 코드네임)
|
||||||
|
|
||||||
|
### [2026-03-08] Antigravity 승인 대기 = RUNNING (NOT IDLE)
|
||||||
|
- **증상**: IDLE 기반 승인 감지가 실제 승인 대기를 놓침
|
||||||
|
- **원인**: 승인 대기 시 세션 상태가 `CASCADE_RUN_STATUS_RUNNING` (IDLE 아님), `IDLE`은 대화 대기(notify_user 후)
|
||||||
|
- **해결**: `RUNNING + delta=0` (stall) 기반 감지로 전환. 6 polls (30초) 이상 FROZEN 시 pending 생성
|
||||||
|
- **주의**: Thinking/생성 중에도 `RUNNING + delta=0`이 발생 → `lastModifiedTime`으로 구분 시도했으나 불완전
|
||||||
|
|
||||||
|
### [2026-03-08] ResolveOutstandingSteps RPC — 승인이 아닌 취소!
|
||||||
|
- **증상**: Discord 승인 → `ResolveOutstandingSteps` 호출 → step이 취소됨
|
||||||
|
- **원인**: `ResolveOutstandingSteps`는 blocking steps를 "resolve" = **REJECT/CANCEL**, approve가 아님
|
||||||
|
- **해결**: `ResolveOutstandingSteps` 제거. `HandleCascadeUserInteraction`은 `socket hang up`
|
||||||
|
- **주의**: KI에 "more reliable"로 기록되어 있으나 실제 동작은 cancel임. KI 업데이트 필요
|
||||||
|
|
||||||
|
### [2026-03-08] VS Code Accept Commands — Silent Success 문제
|
||||||
|
- **증상**: 4개 accept command 모두 OK(undefined) 반환하나 실제 승인 안 됨
|
||||||
|
- **원인**: webview에 활성 포커스가 필요. `panel.focus()`로는 충분하지 않음
|
||||||
|
- **해결**: **미해결**. Windows UI Automation 등 OS 레벨 접근 필요
|
||||||
|
- **주의**: reject commands는 동작함. accept만 focus 의존성 있음
|
||||||
|
|
||||||
|
### [2026-03-08] Multi-Window 세션 등록 경쟁 조건
|
||||||
|
- **증상**: 이 창(gravity_control)의 대화가 `#ag-variet_agent` 채널로 메시지 전달
|
||||||
|
- **원인**: `writeRegistration()`이 폴링 루프에서 호출 → 먼저 폴링한 확장이 세션을 자기 프로젝트로 등록
|
||||||
|
- **해결**: `writeRegistration`을 폴링에서 제거, `writeChatSnapshot`/`writePendingApproval`에서만 지연 호출
|
||||||
|
- **주의**: `GetAllCascadeTrajectories`는 모든 창의 세션을 반환하므로 세션→창 매핑은 불가능. 활동 기반 등록만 신뢰 가능
|
||||||
|
|
||||||
|
### [2026-03-08] 공유 렌더러 스크립트 파일 덮어쓰기 문제
|
||||||
|
- **증상**: DOM Observer 렌더러 스크립트가 잘못된 HTTP bridge 포트에 연결
|
||||||
|
- **원인**: 두 확장이 동일한 `ag-sdk-variet-gravity-bridge.js` 파일에 각자 포트를 씀 → 마지막 확장 것만 남음
|
||||||
|
- **해결**: `ag-bridge-ports.json`에 모든 확장의 port를 JSON으로 기록, 렌더러가 all ports를 순회하며 ping
|
||||||
|
- **주의**: 렌더러 스크립트 파일 경로는 SDK patcher namespace에 의해 고정 — 변경 불가
|
||||||
|
|
||||||
|
### [2026-03-08] workbench.html vs workbench-jetski-agent.html
|
||||||
|
- **증상**: 렌더러에서 `[GB Observer]` 로그가 전혀 안 나옴
|
||||||
|
- **원인**: DevTools가 `workbench.html`을 로드 — 스크립트 태그는 `workbench-jetski-agent.html`에만 패치됨
|
||||||
|
- **해결**: `workbench.html`에도 스크립트 태그 필요. Antigravity 재설치 후 SDK patcher가 올바르게 패치하도록 함
|
||||||
|
- **주의**: SDK patcher는 `both` HTML 파일을 패치하지만, 수동 수정은 Antigravity integrity check에 의해 되돌려질 수 있음
|
||||||
|
|
||||||
|
### [2026-03-08] product.json 체크섬 불일치 → 렌더러 스크립트 미로딩
|
||||||
|
- **증상**: `<script>` 태그가 HTML에 존재하고 .js 파일도 디스크에 있으나, 렌더러 콘솔에 스크립트 로그가 전혀 없음
|
||||||
|
- **원인**: Antigravity 재설치 시 `product.json`의 SHA256 체크섬이 원본으로 리셋됨. Extension이 HTML을 패치하지만 `IntegrityManager.suppressCheck()`를 호출하지 않아 체크섬 불일치. `vscode-file://` 프로토콜이 체크섬 불일치 파일을 무시하고 **원본 캐시 HTML**을 서빙
|
||||||
|
- **해결**: `product.json`의 `checksums` 항목에서 수정된 파일(workbench.html, workbench-jetski-agent.html)의 SHA256 해시를 실제 파일 기준으로 업데이트. SDK `IntegrityManager.suppressCheck()` 호출 또는 수동 스크립트로 해결
|
||||||
|
- **주의**: Extension `setupApprovalObserver()`에 `suppressCheck()` 호출을 영구 추가해야 재설치마다 반복 안 됨. 해시 = `base64(sha256(file)).replace(/=+$/, '')`
|
||||||
|
|
||||||
|
### [2026-03-08] vscode-file:// 프로토콜 — 커스텀 .js 파일 서빙 불가
|
||||||
|
- **증상**: `<script src="./ag-sdk-variet-gravity-bridge.js">` 태그가 HTML에 있으나 `net::ERR_FILE_NOT_FOUND` 발생, GB Observer 로그 전혀 없음
|
||||||
|
- **원인**: `vscode-file://` 프로토콜은 원본 배포에 포함된 파일만 서빙. Extension이 디스크에 쓴 커스텀 `.js` 파일은 프로토콜 레벨에서 차단됨
|
||||||
|
- **해결**: 외부 `<script src>` 참조 대신 **인라인 `<script>...코드...</script>`** 방식으로 HTML에 직접 삽입
|
||||||
|
- **주의**: `ag-bridge-ports.json`도 같은 이유로 XHR 로딩 불가. 모든 렌더러 스크립트/데이터는 HTML 인라인으로 전달해야 함
|
||||||
|
|
||||||
|
### [2026-03-08] Renderer 포트 디스커버리 — ag-bridge-ports.json XHR 실패
|
||||||
|
- **증상**: `[GB Observer] Port discovery timeout after 2min` — 렌더러가 bridge 포트를 찾지 못함
|
||||||
|
- **원인**: 렌더러 스크립트가 `./ag-bridge-ports.json`을 동기 XHR로 읽으려 하나, `vscode-file://` 프로토콜이 `.json` 파일 서빙 거부
|
||||||
|
- **해결**: (1) 프로젝트명 해시 기반 **결정론적 포트** 사용 (`gravity_control→34332`), (2) 스크립트 생성 시 포트를 `HARDCODED_PORT=${port}`로 직접 삽입
|
||||||
|
- **주의**: `server.listen(0)` 랜덤 포트 → 매 재시작마다 변경되어 렌더러와 불일치. 결정론적 포트는 `EADDRINUSE` 시 랜덤 폴백 필요
|
||||||
@@ -16,3 +16,4 @@
|
|||||||
| 12 | 11:30~14:35 | 승인 로직 정밀 디버깅: IDLE→stall 전환, lastModifiedTime 구분, RPC/Commands 전수 테스트, ResolveOutstandingSteps cancel 발견 | - | 🔧 |
|
| 12 | 11:30~14:35 | 승인 로직 정밀 디버깅: IDLE→stall 전환, lastModifiedTime 구분, RPC/Commands 전수 테스트, ResolveOutstandingSteps cancel 발견 | - | 🔧 |
|
||||||
| 13 | 15:00~16:52 | Multi-window 격리 (v0.3.1→0.3.4): 세션 필터, per-project 포트, 등록 경쟁 조건 수정, DOM Observer 렌더러 디버깅 | - | 🔧 |
|
| 13 | 15:00~16:52 | Multi-window 격리 (v0.3.1→0.3.4): 세션 필터, per-project 포트, 등록 경쟁 조건 수정, DOM Observer 렌더러 디버깅 | - | 🔧 |
|
||||||
| 14 | 17:01~17:38 | **근본 원인 발견**: product.json 체크섬 불일치 → vscode-file:// 원본 캐시 서빙. 체크섬 수동 업데이트로 수정 | - | 🔧 |
|
| 14 | 17:01~17:38 | **근본 원인 발견**: product.json 체크섬 불일치 → vscode-file:// 원본 캐시 서빙. 체크섬 수동 업데이트로 수정 | - | 🔧 |
|
||||||
|
| 15 | 17:50~18:30 | **v0.3.5**: 포트 디스커버리 수정 (결정론적 포트 + 하드코딩), 인라인 스크립트 전환 (`<script src>` → `<script>inline</script>`), product.json 자동 체크섬 업데이트 | - | 🔧 |
|
||||||
|
|||||||
Binary file not shown.
@@ -51,6 +51,7 @@ const fs = __importStar(require("fs"));
|
|||||||
const path = __importStar(require("path"));
|
const path = __importStar(require("path"));
|
||||||
const os = __importStar(require("os"));
|
const os = __importStar(require("os"));
|
||||||
const cp = __importStar(require("child_process"));
|
const cp = __importStar(require("child_process"));
|
||||||
|
const crypto = __importStar(require("crypto"));
|
||||||
// ─── File-based logging (AI can read directly) ───
|
// ─── File-based logging (AI can read directly) ───
|
||||||
function logToFile(msg) {
|
function logToFile(msg) {
|
||||||
const ts = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
const ts = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
||||||
@@ -73,6 +74,7 @@ let statusBar;
|
|||||||
let bridgePath;
|
let bridgePath;
|
||||||
let projectName;
|
let projectName;
|
||||||
let isActive = false;
|
let isActive = false;
|
||||||
|
let deterministicPort = 0; // derived from projectName, consistent across restarts
|
||||||
let watcher = null;
|
let watcher = null;
|
||||||
let commandsWatcher = null;
|
let commandsWatcher = null;
|
||||||
const sentPendingIds = new Set();
|
const sentPendingIds = new Set();
|
||||||
@@ -287,26 +289,44 @@ async function setupApprovalObserver() {
|
|||||||
logToFile('[OBSERVER] workbench.html patched (needs reload)');
|
logToFile('[OBSERVER] workbench.html patched (needs reload)');
|
||||||
}
|
}
|
||||||
// Also patch workbench-jetski-agent.html (Antigravity's actual entry point!)
|
// Also patch workbench-jetski-agent.html (Antigravity's actual entry point!)
|
||||||
|
// IMPORTANT: vscode-file:// protocol does NOT serve custom .js files,
|
||||||
|
// so we INLINE the script content directly into the HTML.
|
||||||
const scriptDir = path.dirname(scriptPath);
|
const scriptDir = path.dirname(scriptPath);
|
||||||
const jetskiHtml = path.join(scriptDir, 'workbench-jetski-agent.html');
|
const jetskiHtml = path.join(scriptDir, 'workbench-jetski-agent.html');
|
||||||
const scriptBasename = path.basename(scriptPath);
|
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(jetskiHtml)) {
|
if (fs.existsSync(jetskiHtml)) {
|
||||||
let html = fs.readFileSync(jetskiHtml, 'utf8');
|
let html = fs.readFileSync(jetskiHtml, 'utf8');
|
||||||
if (!html.includes(scriptBasename)) {
|
// Remove old external script tag if present (from previous versions)
|
||||||
html = html.replace('</html>', `\n<!-- AG SDK [variet-gravity-bridge] -->\n<script src="./${scriptBasename}"></script>\n<!-- /AG SDK [variet-gravity-bridge] -->\n</html>`);
|
const oldScriptBasename = path.basename(scriptPath);
|
||||||
fs.writeFileSync(jetskiHtml, html, 'utf8');
|
if (html.includes(`src="./${oldScriptBasename}"`)) {
|
||||||
logToFile('[OBSERVER] workbench-jetski-agent.html PATCHED');
|
html = html.replace(/\n?<!-- AG SDK \[variet-gravity-bridge\] -->\n?<script src="[^"]*"><\/script>\n?<!-- \/AG SDK \[variet-gravity-bridge\] -->\n?/, '');
|
||||||
|
logToFile('[OBSERVER] removed old external <script src> tag from jetski HTML');
|
||||||
|
}
|
||||||
|
// Insert or update inline script
|
||||||
|
const inlineMarkerStart = '<!-- AG SDK INLINE [variet-gravity-bridge] -->';
|
||||||
|
const inlineMarkerEnd = '<!-- /AG SDK INLINE [variet-gravity-bridge] -->';
|
||||||
|
if (html.includes(inlineMarkerStart)) {
|
||||||
|
// Replace existing inline script with updated content
|
||||||
|
const re = new RegExp(inlineMarkerStart.replace(/[[\]]/g, '\\$&') +
|
||||||
|
'[\\s\\S]*?' +
|
||||||
|
inlineMarkerEnd.replace(/[[\]]/g, '\\$&'));
|
||||||
|
html = html.replace(re, `${inlineMarkerStart}\n<script>\n${combinedScript}\n</script>\n${inlineMarkerEnd}`);
|
||||||
|
logToFile('[OBSERVER] jetski HTML inline script UPDATED');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
logToFile('[OBSERVER] workbench-jetski-agent.html already has script tag');
|
// First time: insert before </html>
|
||||||
|
html = html.replace('</html>', `\n${inlineMarkerStart}\n<script>\n${combinedScript}\n</script>\n${inlineMarkerEnd}\n</html>`);
|
||||||
|
logToFile('[OBSERVER] jetski HTML inline script INSERTED');
|
||||||
}
|
}
|
||||||
|
fs.writeFileSync(jetskiHtml, html, 'utf8');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
logToFile(`[OBSERVER] jetski patch error: ${e.message}`);
|
logToFile(`[OBSERVER] jetski patch error: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 4. Update product.json checksums so vscode-file:// serves our patched files
|
||||||
|
updateProductChecksums();
|
||||||
try {
|
try {
|
||||||
integration.enableAutoRepair();
|
integration.enableAutoRepair();
|
||||||
}
|
}
|
||||||
@@ -322,9 +342,78 @@ async function setupApprovalObserver() {
|
|||||||
logToFile(`[OBSERVER] setup error: ${err.message}`);
|
logToFile(`[OBSERVER] setup error: ${err.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// ─── Product.json Checksum Auto-Update ───
|
||||||
|
// vscode-file:// protocol validates SHA256 checksums in product.json.
|
||||||
|
// If a file's checksum doesn't match, Electron serves the ORIGINAL cached version.
|
||||||
|
// This function recalculates checksums for files we modify (HTML files with <script> tags).
|
||||||
|
function updateProductChecksums() {
|
||||||
|
try {
|
||||||
|
// Find product.json (2 levels up from workbench dir: resources/app/product.json)
|
||||||
|
const patcher = sdk?.integration?._patcher;
|
||||||
|
if (!patcher || typeof patcher.getWorkbenchDir !== 'function') {
|
||||||
|
logToFile('[CHECKSUM] no patcher/workbenchDir — skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const workbenchDir = patcher.getWorkbenchDir();
|
||||||
|
// workbenchDir = .../resources/app/out/vs/code/electron-browser/workbench
|
||||||
|
// product.json = .../resources/app/product.json (5 levels up from workbench)
|
||||||
|
const appDir = path.resolve(workbenchDir, '..', '..', '..', '..', '..');
|
||||||
|
const productJsonPath = path.join(appDir, 'product.json');
|
||||||
|
if (!fs.existsSync(productJsonPath)) {
|
||||||
|
logToFile(`[CHECKSUM] product.json not found at ${productJsonPath}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Read product.json (may have BOM)
|
||||||
|
let raw = fs.readFileSync(productJsonPath, 'utf8');
|
||||||
|
if (raw.charCodeAt(0) === 0xFEFF)
|
||||||
|
raw = raw.substring(1);
|
||||||
|
const product = JSON.parse(raw);
|
||||||
|
if (!product.checksums) {
|
||||||
|
logToFile('[CHECKSUM] no checksums section in product.json');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Files we may modify (relative key in product.json → absolute path)
|
||||||
|
const filesToCheck = {
|
||||||
|
'vs/code/electron-browser/workbench/workbench.html': path.join(workbenchDir, 'workbench.html'),
|
||||||
|
'vs/code/electron-browser/workbench/workbench-jetski-agent.html': path.join(workbenchDir, 'workbench-jetski-agent.html'),
|
||||||
|
};
|
||||||
|
let updated = false;
|
||||||
|
for (const [key, filePath] of Object.entries(filesToCheck)) {
|
||||||
|
if (!product.checksums[key])
|
||||||
|
continue; // not in checksums, skip
|
||||||
|
if (!fs.existsSync(filePath))
|
||||||
|
continue;
|
||||||
|
const fileBytes = fs.readFileSync(filePath);
|
||||||
|
const hash = crypto.createHash('sha256').update(fileBytes).digest('base64').replace(/=+$/, '');
|
||||||
|
if (product.checksums[key] !== hash) {
|
||||||
|
logToFile(`[CHECKSUM] updating ${key}: ${product.checksums[key].substring(0, 12)}... → ${hash.substring(0, 12)}...`);
|
||||||
|
product.checksums[key] = hash;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (updated) {
|
||||||
|
fs.writeFileSync(productJsonPath, JSON.stringify(product, null, '\t'), 'utf8');
|
||||||
|
logToFile('[CHECKSUM] product.json updated ✅');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logToFile('[CHECKSUM] all checksums already match ✅');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
logToFile(`[CHECKSUM] error: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
// ─── HTTP Bridge Server (Extension Host → Renderer communication) ───
|
// ─── HTTP Bridge Server (Extension Host → Renderer communication) ───
|
||||||
let observerHttpServer = null;
|
let observerHttpServer = null;
|
||||||
const pendingResponses = new Map();
|
const pendingResponses = new Map();
|
||||||
|
/** Derive a deterministic port from project name (range 10000-60000) */
|
||||||
|
function getDeterministicPort(name) {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < name.length; i++) {
|
||||||
|
hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0;
|
||||||
|
}
|
||||||
|
return 10000 + (Math.abs(hash) % 50000);
|
||||||
|
}
|
||||||
function startObserverHttpBridge() {
|
function startObserverHttpBridge() {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
try {
|
try {
|
||||||
@@ -406,33 +495,53 @@ function startObserverHttpBridge() {
|
|||||||
res.writeHead(404);
|
res.writeHead(404);
|
||||||
res.end('not found');
|
res.end('not found');
|
||||||
});
|
});
|
||||||
// Listen on random port
|
// Listen on deterministic port (derived from projectName), fallback to random
|
||||||
server.listen(0, '127.0.0.1', () => {
|
deterministicPort = getDeterministicPort(projectName);
|
||||||
const port = server.address().port;
|
const tryListen = (targetPort) => {
|
||||||
observerHttpServer = server;
|
server.listen(targetPort, '127.0.0.1', () => {
|
||||||
logToFile(`[HTTP] bridge server started on port ${port}`);
|
const port = server.address().port;
|
||||||
// Write port to shared ports JSON (multi-bridge support)
|
observerHttpServer = server;
|
||||||
const patcher = sdk.integration?._patcher;
|
logToFile(`[HTTP] bridge server started on port ${port}`);
|
||||||
if (patcher && typeof patcher.getWorkbenchDir === 'function') {
|
// Write port to shared ports JSON (multi-bridge support)
|
||||||
const workbenchDir = patcher.getWorkbenchDir();
|
const patcher = sdk.integration?._patcher;
|
||||||
const portsFile = path.join(workbenchDir, 'ag-bridge-ports.json');
|
if (patcher && typeof patcher.getWorkbenchDir === 'function') {
|
||||||
let portsData = {};
|
const workbenchDir = patcher.getWorkbenchDir();
|
||||||
try {
|
const portsFile = path.join(workbenchDir, 'ag-bridge-ports.json');
|
||||||
if (fs.existsSync(portsFile)) {
|
let portsData = {};
|
||||||
portsData = JSON.parse(fs.readFileSync(portsFile, 'utf-8'));
|
try {
|
||||||
|
if (fs.existsSync(portsFile)) {
|
||||||
|
portsData = JSON.parse(fs.readFileSync(portsFile, 'utf-8'));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
catch { }
|
||||||
|
portsData[projectName] = port;
|
||||||
|
fs.writeFileSync(portsFile, JSON.stringify(portsData), 'utf-8');
|
||||||
|
logToFile(`[HTTP] ports JSON updated → ${portsFile} (${projectName}=${port})`);
|
||||||
}
|
}
|
||||||
catch { }
|
resolve(port);
|
||||||
portsData[projectName] = port;
|
});
|
||||||
fs.writeFileSync(portsFile, JSON.stringify(portsData), 'utf-8');
|
};
|
||||||
logToFile(`[HTTP] ports JSON updated → ${portsFile} (${projectName}=${port})`);
|
|
||||||
}
|
|
||||||
resolve(port);
|
|
||||||
});
|
|
||||||
server.on('error', (e) => {
|
server.on('error', (e) => {
|
||||||
|
if (e.code === 'EADDRINUSE' && deterministicPort > 0) {
|
||||||
|
logToFile(`[HTTP] deterministic port ${deterministicPort} in use, trying random...`);
|
||||||
|
deterministicPort = 0;
|
||||||
|
const server2 = require('http').createServer(server._events.request);
|
||||||
|
observerHttpServer = server2;
|
||||||
|
server2.on('error', (e2) => {
|
||||||
|
logToFile(`[HTTP] random port also failed: ${e2.message}`);
|
||||||
|
resolve(0);
|
||||||
|
});
|
||||||
|
server2.listen(0, '127.0.0.1', () => {
|
||||||
|
const port = server2.address().port;
|
||||||
|
logToFile(`[HTTP] bridge server started on RANDOM port ${port}`);
|
||||||
|
resolve(port);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
logToFile(`[HTTP] server error: ${e.message}`);
|
logToFile(`[HTTP] server error: ${e.message}`);
|
||||||
resolve(0);
|
resolve(0);
|
||||||
});
|
});
|
||||||
|
tryListen(deterministicPort);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
logToFile(`[HTTP] server failed: ${e.message}`);
|
logToFile(`[HTTP] server failed: ${e.message}`);
|
||||||
@@ -455,40 +564,67 @@ function generateApprovalObserverScript(_port) {
|
|||||||
function log(m){console.log('[GB Observer] '+m);}
|
function log(m){console.log('[GB Observer] '+m);}
|
||||||
log('v2 Script loaded — discovering bridge port...');
|
log('v2 Script loaded — discovering bridge port...');
|
||||||
|
|
||||||
// ── Multi-Port Discovery: reads ag-bridge-ports.json, tries ALL bridges ──
|
// ── Port Discovery: try hardcoded port FIRST, then JSON fallback ──
|
||||||
|
// vscode-file:// protocol blocks .json files, so XHR to ag-bridge-ports.json
|
||||||
|
// returns 404. The extension embeds the known port at script generation time.
|
||||||
|
var HARDCODED_PORT=${_port};
|
||||||
|
|
||||||
|
function tryPing(port,cb){
|
||||||
|
try{
|
||||||
|
var xhr=new XMLHttpRequest();
|
||||||
|
xhr.open('GET','http://127.0.0.1:'+port+'/ping?t='+Date.now(),false);
|
||||||
|
xhr.timeout=2000;
|
||||||
|
xhr.send();
|
||||||
|
if(xhr.status===200&&xhr.responseText==='pong'){cb(true);return;}
|
||||||
|
}catch(e){}
|
||||||
|
cb(false);
|
||||||
|
}
|
||||||
|
|
||||||
function discoverPort(cb){
|
function discoverPort(cb){
|
||||||
var attempts=0;
|
// Strategy 1: Try hardcoded port immediately
|
||||||
var timer=setInterval(function(){
|
log('Trying hardcoded port '+HARDCODED_PORT+'...');
|
||||||
attempts++;
|
tryPing(HARDCODED_PORT,function(ok){
|
||||||
if(attempts>60){clearInterval(timer);log('Port discovery timeout after 2min');return;}
|
if(ok){log('Port discovered (hardcoded): '+HARDCODED_PORT);cb(HARDCODED_PORT);return;}
|
||||||
try{
|
log('Hardcoded port failed, falling back to JSON + port scan...');
|
||||||
var xhr=new XMLHttpRequest();
|
|
||||||
xhr.open('GET','./ag-bridge-ports.json?t='+Date.now(),false);
|
// Strategy 2: JSON file fallback + port scan
|
||||||
xhr.send();
|
var attempts=0;
|
||||||
if(xhr.status===200){
|
var timer=setInterval(function(){
|
||||||
var ports=JSON.parse(xhr.responseText);
|
attempts++;
|
||||||
var keys=Object.keys(ports);
|
if(attempts>60){clearInterval(timer);log('Port discovery timeout after 2min');return;}
|
||||||
for(var i=0;i<keys.length;i++){
|
// Try hardcoded port again (server may start late)
|
||||||
var port=ports[keys[i]];
|
tryPing(HARDCODED_PORT,function(ok2){
|
||||||
if(port>0&&port<65536){
|
if(ok2){clearInterval(timer);log('Port discovered (hardcoded retry): '+HARDCODED_PORT);cb(HARDCODED_PORT);return;}
|
||||||
// Try ping on each port
|
});
|
||||||
try{
|
// Try JSON file (may work in future Electron versions)
|
||||||
var xhr2=new XMLHttpRequest();
|
try{
|
||||||
xhr2.open('GET','http://127.0.0.1:'+port+'/ping?t='+Date.now(),false);
|
var xhr=new XMLHttpRequest();
|
||||||
xhr2.timeout=1000;
|
xhr.open('GET','./ag-bridge-ports.json?t='+Date.now(),false);
|
||||||
xhr2.send();
|
xhr.send();
|
||||||
if(xhr2.status===200&&xhr2.responseText==='pong'){
|
if(xhr.status===200){
|
||||||
clearInterval(timer);
|
var ports=JSON.parse(xhr.responseText);
|
||||||
log('Port discovered: '+port+' (project='+keys[i]+')');
|
var keys=Object.keys(ports);
|
||||||
cb(port);
|
for(var i=0;i<keys.length;i++){
|
||||||
return;
|
var port=ports[keys[i]];
|
||||||
}
|
if(port>0&&port<65536){
|
||||||
}catch(e2){}
|
try{
|
||||||
|
var xhr2=new XMLHttpRequest();
|
||||||
|
xhr2.open('GET','http://127.0.0.1:'+port+'/ping?t='+Date.now(),false);
|
||||||
|
xhr2.timeout=1000;
|
||||||
|
xhr2.send();
|
||||||
|
if(xhr2.status===200&&xhr2.responseText==='pong'){
|
||||||
|
clearInterval(timer);
|
||||||
|
log('Port discovered (JSON): '+port+' (project='+keys[i]+')');
|
||||||
|
cb(port);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}catch(e2){}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}catch(e){}
|
||||||
}catch(e){}
|
},2000);
|
||||||
},2000);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
discoverPort(function(port){
|
discoverPort(function(port){
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -2,7 +2,7 @@
|
|||||||
"name": "gravity-bridge",
|
"name": "gravity-bridge",
|
||||||
"displayName": "Gravity Bridge",
|
"displayName": "Gravity Bridge",
|
||||||
"description": "Antigravity ↔ Discord 브리지 연동 확장",
|
"description": "Antigravity ↔ Discord 브리지 연동 확장",
|
||||||
"version": "0.2.0",
|
"version": "0.3.5",
|
||||||
"publisher": "variet",
|
"publisher": "variet",
|
||||||
"engines": {
|
"engines": {
|
||||||
"vscode": "^1.100.0"
|
"vscode": "^1.100.0"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import * as cp from 'child_process';
|
import * as cp from 'child_process';
|
||||||
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
// ─── File-based logging (AI can read directly) ───
|
// ─── File-based logging (AI can read directly) ───
|
||||||
function logToFile(msg: string) {
|
function logToFile(msg: string) {
|
||||||
@@ -38,6 +39,7 @@ let statusBar: vscode.StatusBarItem;
|
|||||||
let bridgePath: string;
|
let bridgePath: string;
|
||||||
let projectName: string;
|
let projectName: string;
|
||||||
let isActive = false;
|
let isActive = false;
|
||||||
|
let deterministicPort = 0; // derived from projectName, consistent across restarts
|
||||||
let watcher: fs.FSWatcher | null = null;
|
let watcher: fs.FSWatcher | null = null;
|
||||||
let commandsWatcher: fs.FSWatcher | null = null;
|
let commandsWatcher: fs.FSWatcher | null = null;
|
||||||
|
|
||||||
@@ -248,26 +250,54 @@ async function setupApprovalObserver() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Also patch workbench-jetski-agent.html (Antigravity's actual entry point!)
|
// Also patch workbench-jetski-agent.html (Antigravity's actual entry point!)
|
||||||
|
// IMPORTANT: vscode-file:// protocol does NOT serve custom .js files,
|
||||||
|
// so we INLINE the script content directly into the HTML.
|
||||||
const scriptDir = path.dirname(scriptPath);
|
const scriptDir = path.dirname(scriptPath);
|
||||||
const jetskiHtml = path.join(scriptDir, 'workbench-jetski-agent.html');
|
const jetskiHtml = path.join(scriptDir, 'workbench-jetski-agent.html');
|
||||||
const scriptBasename = path.basename(scriptPath);
|
|
||||||
try {
|
try {
|
||||||
if (fs.existsSync(jetskiHtml)) {
|
if (fs.existsSync(jetskiHtml)) {
|
||||||
let html = fs.readFileSync(jetskiHtml, 'utf8');
|
let html = fs.readFileSync(jetskiHtml, 'utf8');
|
||||||
if (!html.includes(scriptBasename)) {
|
|
||||||
html = html.replace('</html>',
|
// Remove old external script tag if present (from previous versions)
|
||||||
`\n<!-- AG SDK [variet-gravity-bridge] -->\n<script src="./${scriptBasename}"></script>\n<!-- /AG SDK [variet-gravity-bridge] -->\n</html>`);
|
const oldScriptBasename = path.basename(scriptPath);
|
||||||
fs.writeFileSync(jetskiHtml, html, 'utf8');
|
if (html.includes(`src="./${oldScriptBasename}"`)) {
|
||||||
logToFile('[OBSERVER] workbench-jetski-agent.html PATCHED');
|
html = html.replace(
|
||||||
} else {
|
/\n?<!-- AG SDK \[variet-gravity-bridge\] -->\n?<script src="[^"]*"><\/script>\n?<!-- \/AG SDK \[variet-gravity-bridge\] -->\n?/,
|
||||||
logToFile('[OBSERVER] workbench-jetski-agent.html already has script tag');
|
''
|
||||||
|
);
|
||||||
|
logToFile('[OBSERVER] removed old external <script src> tag from jetski HTML');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Insert or update inline script
|
||||||
|
const inlineMarkerStart = '<!-- AG SDK INLINE [variet-gravity-bridge] -->';
|
||||||
|
const inlineMarkerEnd = '<!-- /AG SDK INLINE [variet-gravity-bridge] -->';
|
||||||
|
|
||||||
|
if (html.includes(inlineMarkerStart)) {
|
||||||
|
// Replace existing inline script with updated content
|
||||||
|
const re = new RegExp(
|
||||||
|
inlineMarkerStart.replace(/[[\]]/g, '\\$&') +
|
||||||
|
'[\\s\\S]*?' +
|
||||||
|
inlineMarkerEnd.replace(/[[\]]/g, '\\$&')
|
||||||
|
);
|
||||||
|
html = html.replace(re,
|
||||||
|
`${inlineMarkerStart}\n<script>\n${combinedScript}\n</script>\n${inlineMarkerEnd}`);
|
||||||
|
logToFile('[OBSERVER] jetski HTML inline script UPDATED');
|
||||||
|
} else {
|
||||||
|
// First time: insert before </html>
|
||||||
|
html = html.replace('</html>',
|
||||||
|
`\n${inlineMarkerStart}\n<script>\n${combinedScript}\n</script>\n${inlineMarkerEnd}\n</html>`);
|
||||||
|
logToFile('[OBSERVER] jetski HTML inline script INSERTED');
|
||||||
|
}
|
||||||
|
fs.writeFileSync(jetskiHtml, html, 'utf8');
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logToFile(`[OBSERVER] jetski patch error: ${e.message}`);
|
logToFile(`[OBSERVER] jetski patch error: ${e.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Update product.json checksums so vscode-file:// serves our patched files
|
||||||
|
updateProductChecksums();
|
||||||
|
|
||||||
try { integration.enableAutoRepair(); } catch { }
|
try { integration.enableAutoRepair(); } catch { }
|
||||||
setInterval(() => { try { integration.signalActive(); } catch { } }, 30_000);
|
setInterval(() => { try { integration.signalActive(); } catch { } }, 30_000);
|
||||||
|
|
||||||
@@ -278,11 +308,86 @@ async function setupApprovalObserver() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Product.json Checksum Auto-Update ───
|
||||||
|
// vscode-file:// protocol validates SHA256 checksums in product.json.
|
||||||
|
// If a file's checksum doesn't match, Electron serves the ORIGINAL cached version.
|
||||||
|
// This function recalculates checksums for files we modify (HTML files with <script> tags).
|
||||||
|
|
||||||
|
function updateProductChecksums() {
|
||||||
|
try {
|
||||||
|
// Find product.json (2 levels up from workbench dir: resources/app/product.json)
|
||||||
|
const patcher = (sdk?.integration as any)?._patcher;
|
||||||
|
if (!patcher || typeof patcher.getWorkbenchDir !== 'function') {
|
||||||
|
logToFile('[CHECKSUM] no patcher/workbenchDir — skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const workbenchDir = patcher.getWorkbenchDir();
|
||||||
|
// workbenchDir = .../resources/app/out/vs/code/electron-browser/workbench
|
||||||
|
// product.json = .../resources/app/product.json (5 levels up from workbench)
|
||||||
|
const appDir = path.resolve(workbenchDir, '..', '..', '..', '..', '..');
|
||||||
|
const productJsonPath = path.join(appDir, 'product.json');
|
||||||
|
|
||||||
|
if (!fs.existsSync(productJsonPath)) {
|
||||||
|
logToFile(`[CHECKSUM] product.json not found at ${productJsonPath}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read product.json (may have BOM)
|
||||||
|
let raw = fs.readFileSync(productJsonPath, 'utf8');
|
||||||
|
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.substring(1);
|
||||||
|
const product = JSON.parse(raw);
|
||||||
|
|
||||||
|
if (!product.checksums) {
|
||||||
|
logToFile('[CHECKSUM] no checksums section in product.json');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Files we may modify (relative key in product.json → absolute path)
|
||||||
|
const filesToCheck: Record<string, string> = {
|
||||||
|
'vs/code/electron-browser/workbench/workbench.html': path.join(workbenchDir, 'workbench.html'),
|
||||||
|
'vs/code/electron-browser/workbench/workbench-jetski-agent.html': path.join(workbenchDir, 'workbench-jetski-agent.html'),
|
||||||
|
};
|
||||||
|
|
||||||
|
let updated = false;
|
||||||
|
for (const [key, filePath] of Object.entries(filesToCheck)) {
|
||||||
|
if (!product.checksums[key]) continue; // not in checksums, skip
|
||||||
|
if (!fs.existsSync(filePath)) continue;
|
||||||
|
|
||||||
|
const fileBytes = fs.readFileSync(filePath);
|
||||||
|
const hash = crypto.createHash('sha256').update(fileBytes).digest('base64').replace(/=+$/, '');
|
||||||
|
|
||||||
|
if (product.checksums[key] !== hash) {
|
||||||
|
logToFile(`[CHECKSUM] updating ${key}: ${product.checksums[key].substring(0, 12)}... → ${hash.substring(0, 12)}...`);
|
||||||
|
product.checksums[key] = hash;
|
||||||
|
updated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
fs.writeFileSync(productJsonPath, JSON.stringify(product, null, '\t'), 'utf8');
|
||||||
|
logToFile('[CHECKSUM] product.json updated ✅');
|
||||||
|
} else {
|
||||||
|
logToFile('[CHECKSUM] all checksums already match ✅');
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
logToFile(`[CHECKSUM] error: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── HTTP Bridge Server (Extension Host → Renderer communication) ───
|
// ─── HTTP Bridge Server (Extension Host → Renderer communication) ───
|
||||||
|
|
||||||
let observerHttpServer: any = null;
|
let observerHttpServer: any = null;
|
||||||
const pendingResponses = new Map<string, { approved: boolean } | null>();
|
const pendingResponses = new Map<string, { approved: boolean } | null>();
|
||||||
|
|
||||||
|
/** Derive a deterministic port from project name (range 10000-60000) */
|
||||||
|
function getDeterministicPort(name: string): number {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < name.length; i++) {
|
||||||
|
hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0;
|
||||||
|
}
|
||||||
|
return 10000 + (Math.abs(hash) % 50000);
|
||||||
|
}
|
||||||
|
|
||||||
function startObserverHttpBridge(): Promise<number> {
|
function startObserverHttpBridge(): Promise<number> {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
try {
|
try {
|
||||||
@@ -357,8 +462,10 @@ function startObserverHttpBridge(): Promise<number> {
|
|||||||
res.writeHead(404); res.end('not found');
|
res.writeHead(404); res.end('not found');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen on random port
|
// Listen on deterministic port (derived from projectName), fallback to random
|
||||||
server.listen(0, '127.0.0.1', () => {
|
deterministicPort = getDeterministicPort(projectName);
|
||||||
|
const tryListen = (targetPort: number) => {
|
||||||
|
server.listen(targetPort, '127.0.0.1', () => {
|
||||||
const port = server.address().port;
|
const port = server.address().port;
|
||||||
observerHttpServer = server;
|
observerHttpServer = server;
|
||||||
logToFile(`[HTTP] bridge server started on port ${port}`);
|
logToFile(`[HTTP] bridge server started on port ${port}`);
|
||||||
@@ -380,12 +487,31 @@ function startObserverHttpBridge(): Promise<number> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolve(port);
|
resolve(port);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
server.on('error', (e: any) => {
|
server.on('error', (e: any) => {
|
||||||
|
if (e.code === 'EADDRINUSE' && deterministicPort > 0) {
|
||||||
|
logToFile(`[HTTP] deterministic port ${deterministicPort} in use, trying random...`);
|
||||||
|
deterministicPort = 0;
|
||||||
|
const server2 = require('http').createServer(server._events.request);
|
||||||
|
observerHttpServer = server2;
|
||||||
|
server2.on('error', (e2: any) => {
|
||||||
|
logToFile(`[HTTP] random port also failed: ${e2.message}`);
|
||||||
|
resolve(0);
|
||||||
|
});
|
||||||
|
server2.listen(0, '127.0.0.1', () => {
|
||||||
|
const port = server2.address().port;
|
||||||
|
logToFile(`[HTTP] bridge server started on RANDOM port ${port}`);
|
||||||
|
resolve(port);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
logToFile(`[HTTP] server error: ${e.message}`);
|
logToFile(`[HTTP] server error: ${e.message}`);
|
||||||
resolve(0);
|
resolve(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tryListen(deterministicPort);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logToFile(`[HTTP] server failed: ${e.message}`);
|
logToFile(`[HTTP] server failed: ${e.message}`);
|
||||||
resolve(0);
|
resolve(0);
|
||||||
@@ -409,40 +535,67 @@ function generateApprovalObserverScript(_port: number): string {
|
|||||||
function log(m){console.log('[GB Observer] '+m);}
|
function log(m){console.log('[GB Observer] '+m);}
|
||||||
log('v2 Script loaded — discovering bridge port...');
|
log('v2 Script loaded — discovering bridge port...');
|
||||||
|
|
||||||
// ── Multi-Port Discovery: reads ag-bridge-ports.json, tries ALL bridges ──
|
// ── Port Discovery: try hardcoded port FIRST, then JSON fallback ──
|
||||||
|
// vscode-file:// protocol blocks .json files, so XHR to ag-bridge-ports.json
|
||||||
|
// returns 404. The extension embeds the known port at script generation time.
|
||||||
|
var HARDCODED_PORT=${_port};
|
||||||
|
|
||||||
|
function tryPing(port,cb){
|
||||||
|
try{
|
||||||
|
var xhr=new XMLHttpRequest();
|
||||||
|
xhr.open('GET','http://127.0.0.1:'+port+'/ping?t='+Date.now(),false);
|
||||||
|
xhr.timeout=2000;
|
||||||
|
xhr.send();
|
||||||
|
if(xhr.status===200&&xhr.responseText==='pong'){cb(true);return;}
|
||||||
|
}catch(e){}
|
||||||
|
cb(false);
|
||||||
|
}
|
||||||
|
|
||||||
function discoverPort(cb){
|
function discoverPort(cb){
|
||||||
var attempts=0;
|
// Strategy 1: Try hardcoded port immediately
|
||||||
var timer=setInterval(function(){
|
log('Trying hardcoded port '+HARDCODED_PORT+'...');
|
||||||
attempts++;
|
tryPing(HARDCODED_PORT,function(ok){
|
||||||
if(attempts>60){clearInterval(timer);log('Port discovery timeout after 2min');return;}
|
if(ok){log('Port discovered (hardcoded): '+HARDCODED_PORT);cb(HARDCODED_PORT);return;}
|
||||||
try{
|
log('Hardcoded port failed, falling back to JSON + port scan...');
|
||||||
var xhr=new XMLHttpRequest();
|
|
||||||
xhr.open('GET','./ag-bridge-ports.json?t='+Date.now(),false);
|
// Strategy 2: JSON file fallback + port scan
|
||||||
xhr.send();
|
var attempts=0;
|
||||||
if(xhr.status===200){
|
var timer=setInterval(function(){
|
||||||
var ports=JSON.parse(xhr.responseText);
|
attempts++;
|
||||||
var keys=Object.keys(ports);
|
if(attempts>60){clearInterval(timer);log('Port discovery timeout after 2min');return;}
|
||||||
for(var i=0;i<keys.length;i++){
|
// Try hardcoded port again (server may start late)
|
||||||
var port=ports[keys[i]];
|
tryPing(HARDCODED_PORT,function(ok2){
|
||||||
if(port>0&&port<65536){
|
if(ok2){clearInterval(timer);log('Port discovered (hardcoded retry): '+HARDCODED_PORT);cb(HARDCODED_PORT);return;}
|
||||||
// Try ping on each port
|
});
|
||||||
try{
|
// Try JSON file (may work in future Electron versions)
|
||||||
var xhr2=new XMLHttpRequest();
|
try{
|
||||||
xhr2.open('GET','http://127.0.0.1:'+port+'/ping?t='+Date.now(),false);
|
var xhr=new XMLHttpRequest();
|
||||||
xhr2.timeout=1000;
|
xhr.open('GET','./ag-bridge-ports.json?t='+Date.now(),false);
|
||||||
xhr2.send();
|
xhr.send();
|
||||||
if(xhr2.status===200&&xhr2.responseText==='pong'){
|
if(xhr.status===200){
|
||||||
clearInterval(timer);
|
var ports=JSON.parse(xhr.responseText);
|
||||||
log('Port discovered: '+port+' (project='+keys[i]+')');
|
var keys=Object.keys(ports);
|
||||||
cb(port);
|
for(var i=0;i<keys.length;i++){
|
||||||
return;
|
var port=ports[keys[i]];
|
||||||
}
|
if(port>0&&port<65536){
|
||||||
}catch(e2){}
|
try{
|
||||||
|
var xhr2=new XMLHttpRequest();
|
||||||
|
xhr2.open('GET','http://127.0.0.1:'+port+'/ping?t='+Date.now(),false);
|
||||||
|
xhr2.timeout=1000;
|
||||||
|
xhr2.send();
|
||||||
|
if(xhr2.status===200&&xhr2.responseText==='pong'){
|
||||||
|
clearInterval(timer);
|
||||||
|
log('Port discovered (JSON): '+port+' (project='+keys[i]+')');
|
||||||
|
cb(port);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}catch(e2){}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}catch(e){}
|
||||||
}catch(e){}
|
},2000);
|
||||||
},2000);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
discoverPort(function(port){
|
discoverPort(function(port){
|
||||||
|
|||||||
Reference in New Issue
Block a user