@lensmcp/cluster 1.25.38 → 1.25.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/executors/gateway/prod-runtime/handler.js +1 -1
- package/executors/gateway/prod-runtime/observability.d.ts +4 -2
- package/executors/gateway/prod-runtime/observability.js +1 -1
- package/executors/gateway/runtime/awake.d.ts +42 -0
- package/executors/gateway/runtime/awake.js +1 -0
- package/executors/gateway/runtime/control.d.ts +11 -1
- package/executors/gateway/runtime/control.js +2 -2
- package/executors/gateway/runtime/lifecycle.d.ts +4 -1
- package/executors/gateway/runtime/lifecycle.js +3 -3
- package/executors/gateway/runtime/observability.d.ts +8 -3
- package/executors/gateway/runtime/observability.js +3 -3
- package/executors/gateway/runtime/proxy.js +1 -1
- package/executors/gateway/runtime/server.js +2 -2
- package/executors/gateway/runtime/types.d.ts +24 -0
- package/executors/gateway/runtime/warm-start.d.ts +18 -0
- package/executors/gateway/runtime/warm-start.js +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var W=Object.defineProperty;var
|
|
1
|
+
"use strict";var W=Object.defineProperty;var C=(y,a)=>W(y,"name",{value:a,configurable:!0});var z=Object.defineProperty,w=C((y,a)=>z(y,"name",{value:a,configurable:!0}),"w");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createHandler=createHandler;const manifest_1=require("../manifest"),gateway_errors_1=require("../gateway-errors"),edge_1=require("./edge");function createHandler(y){const{rt:a,obs:c,cors:M,auth:L,upstream:D,rollout:b,hooks:h,reqState:T,spanByReq:$,ctxByReq:V}=y,j=!!a.opts.pods?.resolveVersion,O=!!a.opts.cookie?.secret,I=!!a.opts.pods?.bucket&&(O||!!a.opts.uidHeader),B=j||O||I,F=a.opts.upstreamRetries??1,G=w(l=>(0,edge_1.tokenMatches)(l.query?.token??(0,edge_1.hdr)(l.raw.headers["x-status-token"]),a.opts.statusToken),"statusTokenMatches"),K=w((l,t,e,v,m)=>{const n=l.raw;if(M.setCors(t,n),t.header("cache-control",edge_1.NO_STORE),n.method==="OPTIONS"){t.code(204).header("access-control-allow-methods","GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS").header("access-control-allow-headers",String(n.headers["access-control-request-headers"]??"*")).send();return}const o=a.table.routes.filter(i=>i.host&&(0,manifest_1.hostMatches)(i.host,e)&&i.prefix),f=m;if(o.length>0&&(f==="/health"||f==="/healthz")){t.code(200).type("application/json").send(JSON.stringify({status:"ok",host:e,services:o.map(i=>({service:i.service,prefix:i.prefix}))}));return}if(f==="/.well-known/lensmcp-routes"){if(!G(l)){(0,edge_1.sendError)(t,n,404,"not_found","No gateway route for this hostname.");return}t.code(200).type("application/json").send(JSON.stringify({host:e,routes:a.table.routes.filter(i=>!i.host||(0,manifest_1.hostMatches)(i.host,e)).map(i=>({host:i.host??"(default)",service:i.service,prefix:i.prefix,internal:!!i.internal,auth:i.auth}))}));return}(0,edge_1.sendError)(t,n,404,"not_found",o.length>0?`No service mounted at ${v} on ${e}.`:"No gateway route for this hostname.")},"handleNoRoute");return async(l,t)=>{const e=l.raw,v=(e.headers.host||"").split(":")[0],m=e.url??"/",n=c.beginTrace(e);(0,gateway_errors_1.ensureRequestId)(e),c.step(n,"received",{host:v,method:e.method,url:m});const o=h.any?h.httpContext(e,l,t,n):void 0;if(o&&V.set(l,o),o&&h.has("onRequest")&&(await h.run("onRequest",o),o.responded()))return t;if(e.httpVersionMajor<1)return c.finish(n,"gateway",400),(0,edge_1.sendError)(t,e,400,"bad_request","Bad Request");const f=(0,edge_1.canonicalize)(m);if(f===void 0)return c.step(n,"bad-path",{}),c.finish(n,"gateway",400),(0,edge_1.sendError)(t,e,400,"bad_request","Bad Request");const i=f.policy,k=f.path+f.query;k!==m&&(0,edge_1.rememberRawTarget)(e,m),e.url=k;const U=e.socket?.remoteAddress??l.ip,S=b.clientIpOf(e,U);if(a.opts.rateLimiter){const r=await a.opts.rateLimiter.check(S);if(!r.allowed)return t.header("retry-after",String(Math.ceil(r.retryAfterMs/1e3))),c.step(n,"rate-limited",{}),c.finish(n,"gateway",429),(0,edge_1.sendError)(t,e,429,"rate_limited","Too Many Requests")}const s=(0,manifest_1.matchRoute)(a.table.routes,v,i+f.query);if(!s)return K(l,t,v,k,i),t;if(o&&(o.route=s),c.step(n,"route-match",{project:s.service,...s.prefix?{prefix:s.prefix}:{},...s.internal?{internal:!0}:{}}),e.method==="OPTIONS")return M.setCors(t,e),t.code(204).header("access-control-allow-methods","GET,POST,PUT,PATCH,DELETE,HEAD,OPTIONS").header("access-control-allow-headers",String(e.headers["access-control-request-headers"]??"*")).send(),t;const E=a.opts.tracing?.startSpan(e,`gateway ${s.service}`,{"http.request.method":e.method??"GET","url.path":k.split("?")[0],"server.address":v,"lensmcp.service":s.service,...s.internal?{"lensmcp.internal":!0}:{}});if(E&&$.set(l,E),o&&h.has("beforeAuth")&&(await h.run("beforeAuth",o),o.responded()))return t;const P=s.internal?void 0:a.opts.identify?.(e),A=P instanceof Promise?await P:P;let R;if(s.internal){const r=L.internal(e,s,n);if(!r.ok)return c.finish(n,s.service,r.status),(0,edge_1.sendError)(t,e,r.status,r.reason,r.detail);R=r.caller,o&&(o.caller=R)}else{const r=L.public(e,s,i,A,S,n);if(!r.ok)return c.finish(n,s.service,r.status),(0,edge_1.sendError)(t,e,r.status,r.reason,r.detail);if(o&&(o.claims=A),o&&h.has("afterAuth")&&(await h.run("afterAuth",o),o.responded()))return t}if(e.headers["x-forwarded-for"]=b.forwardedFor(e,U),e.headers["x-forwarded-proto"]=a.opts.tls?"https":"http",e.headers["x-forwarded-host"]=e.headers.host??v,a.opts.serviceKeys?.[s.service]&&(e.headers["x-internal-token"]=a.opts.serviceKeys[s.service]),s.prependPrefix){const r=e.url??"/",d=s.prependPrefix;r!==d&&!r.startsWith(d+"/")&&(e.url=d+r)}const x=(0,edge_1.hdr)(e.headers[a.versionHeader]);let H=x,_=!1,q;if(B&&!s.internal&&!x){const r=b.resolveStickyKey(e);r.mint&&(q=r.key);const d=j?a.opts.pods.resolveVersion(s.service,b.buildAttrs(e,S,A,r.key)):void 0;if(d)H=d.version,_=!0,c.step(n,"targeting",{rule:d.rule,version:d.version});else if(r.key){const g=a.opts.pods?.bucket?.(s.service,r.key);g&&(H=g.version,_=!0,c.step(n,"targeting",{mode:"sticky",version:g.version}))}}const p=await D.resolveUpstream(s,n,H,_);if(!p)return c.finish(n,s.service,503),(0,edge_1.sendError)(t,e,503,"unavailable",`Service ${s.service} has no available upstream.`);if(!("url"in p))return c.finish(n,s.service,502),(0,edge_1.sendError)(t,e,502,"unavailable",`Service ${s.service}: socket upstreams are not supported by the prod gateway.`);const J=new URL(p.url).origin,N=p.url,u={route:s,caller:R,startedAt:Date.now(),trace:n,version:p.version,upLabel:N};return T.set(l,u),t.raw.once("close",()=>{if(t.raw.writableFinished||T.get(l)!==u)return;T.delete(l);const r=t.raw.headersSent,d=r?t.raw.statusCode:void 0;c.recordEdge(u.route,Date.now()-u.startedAt,d,u.caller,u.version),c.finish(u.trace,u.route.service,d,{aborted:r?"during-response":"before-response",error:r?"the caller went away before the response ended":"the caller went away before any response was sent",upstream:u.upLabel,...u.version?{version:u.version}:{},...u.caller?{caller:u.caller}:{}})}),o&&(o.version=p.version),c.step(n,p.version?"rollout":"upstream",{upstream:N,...p.version?{version:p.version,...x?{pinned:!0}:{}}:{}}),E&&a.opts.tracing.inject(E,e.headers),o&&h.has("beforeForward")&&(await h.run("beforeForward",o),o.responded())?t:t.from(void 0,{getUpstream:w(()=>J,"getUpstream"),retriesCount:F,rewriteHeaders:w(r=>{if(u.proxied=!0,n&&(r["x-lensmcp-upstream"]=N),r["x-request-id"]||(r["x-request-id"]=(0,gateway_errors_1.ensureRequestId)(e)),p.version&&(r["x-lensmcp-version"]=p.version),(x||p.version)&&(0,edge_1.addVary)(r,a.versionHeader),(q||I&&O)&&(0,edge_1.addVary)(r,"Cookie"),delete r["x-powered-by"],delete r.connection,delete r["keep-alive"],delete r["proxy-connection"],r.server="LensMCP",q){const d=r["set-cookie"],g=Array.isArray(d)?d:d?[d]:[];r["set-cookie"]=[...g,b.mintCookieHeader(q)]}return r},"rewriteHeaders"),onError:w((r,{error:d})=>{a.opts.pods?.drop?.(s.service,p),o&&h.has("onError")&&(o.error=d,h.run("onError",o)),(0,edge_1.sendError)(r,e,502,"unavailable",`Upstream ${s.service} unavailable.`)},"onError")})}}C(createHandler,"createHandler"),w(createHandler,"createHandler");
|
|
@@ -13,8 +13,10 @@ export interface Observability {
|
|
|
13
13
|
emit(severity: string, title: string, fingerprint: string, raw?: Record<string, unknown>, ctx?: Record<string, unknown>): void;
|
|
14
14
|
beginTrace(req: http.IncomingMessage): GwTrace | undefined;
|
|
15
15
|
step(t: GwTrace | undefined, name: string, info?: Record<string, unknown>): void;
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
/** `status` `undefined`: the caller went away before any response head — the span reads «no response» (foodguard-118). */
|
|
17
|
+
finish(t: GwTrace | undefined, service: string, status: number | undefined, extra?: Record<string, unknown>): void;
|
|
18
|
+
/** `status` `undefined`: the caller went away before any response — timed, but not an upstream error. */
|
|
19
|
+
recordEdge(route: Route, ms: number, status: number | undefined, caller?: string, version?: string): void;
|
|
18
20
|
buildStatusSnapshot(): ServiceStatus[];
|
|
19
21
|
/** Start the periodic edge-flush + health-window roll; returns the interval (`.unref()`'d). */
|
|
20
22
|
startFlusher(): NodeJS.Timeout;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var S=Object.defineProperty;var p=(a,d)=>S(a,"name",{value:d,configurable:!0});var y=Object.defineProperty,c=p((a,d)=>y(a,"name",{value:d,configurable:!0}),"c");Object.defineProperty(exports,"__esModule",{value:!0}),exports.createObservability=createObservability;const redact_1=require("@lensmcp/core/redact"),edge_1=require("./edge");function createObservability(a,d){const v=c((e,s,t,r,o)=>{if(a.emit)try{a.emit({id:(0,edge_1.eid)(),sessionId:"pending",timestamp:Date.now(),source:"gateway",category:"cluster",severity:e,context:(0,redact_1.redactDeep)({sessionId:"pending",...o}),fingerprint:t,title:(0,redact_1.safeTitle)(s),raw:r&&(0,redact_1.redactDeep)(r)})}catch{}},"emit"),h=new Map,m=c((e,s,t,r,o)=>{const n=e.host??"(default)",u=(r?r+"\u21D2":"")+n+"\u2192"+e.service+(o?"@"+o:""),i=h.get(u)??{host:n,service:e.service,caller:r,version:o,count:0,totalMs:0,errors:0};i.count+=1,i.totalMs+=s,t!==void 0&&(t>=500||t===0)&&(i.errors+=1),h.set(u,i)},"recordEdge"),q=Math.max(1,a.statusWindows??12),f=[],w=new Map,M=c(()=>[...new Set(d().map(e=>e.service))],"routeServices"),$=c(()=>{const e=new Map;for(const t of h.values()){const r=e.get(t.service)??{requests:0,errors:0,totalMs:0,versions:new Set};r.requests+=t.count,r.errors+=t.errors,r.totalMs+=t.totalMs,t.version&&r.versions.add(t.version),e.set(t.service,r)}const s=Date.now();for(const[t,r]of e)r.requests>0&&w.set(t,s);for(f.push(e);f.length>q;)f.shift()},"pushStatusWindow"),g=c(()=>{const e=new Map;for(const t of f)for(const[r,o]of t){const n=e.get(r)??{requests:0,errors:0,totalMs:0,versions:new Set};n.requests+=o.requests,n.errors+=o.errors,n.totalMs+=o.totalMs;for(const u of o.versions)n.versions.add(u);e.set(r,n)}const s=d();return M().map(t=>{const r=e.get(t)??{requests:0,errors:0,totalMs:0,versions:new Set},o=[...new Set(s.filter(i=>i.service===t).map(i=>i.host??"(default)"))],n=r.requests?r.errors/r.requests:0;let u=r.requests===0?"idle":n>=.5?"down":n>=.05?"degraded":"healthy";if(a.healthChecker){const i=s.filter(l=>l.service===t&&l.upstream).map(l=>l.upstream);i.length&&i.every(l=>!a.healthChecker.isHealthy(l))&&(u="down")}return{service:t,hosts:o,status:u,requests:r.requests,errorRate:+n.toFixed(4),avgMs:r.requests?Math.round(r.totalMs/r.requests):0,versions:[...r.versions],lastSeen:w.get(t)??null}})},"buildStatusSnapshot");return{emit:v,beginTrace:c(e=>{if(!a.emit)return;const s=(0,edge_1.hdr)(e.headers["x-lensmcp-flow-id"]);let t=(0,edge_1.hdr)(e.headers["x-request-id"]);if(!(!s&&!t&&!(0,edge_1.hdr)(e.headers.traceparent)))return t||(t=(0,edge_1.eid)(),e.headers["x-request-id"]=t),{startedAt:Date.now(),method:(e.method??"GET").toUpperCase(),host:(e.headers.host||"").split(":")[0],url:e.url??"/",steps:[],...s?{flowId:s}:{},...t?{requestId:t}:{}}},"beginTrace"),step:c((e,s,t)=>{e&&e.steps.push({step:s,atMs:Date.now()-e.startedAt,...t})},"step"),finish:c((e,s,t,r)=>{if(!e||e.done)return;e.done=!0;const o=Date.now()-e.startedAt;v(t===void 0?"warning":t>=500?"error":"info",t===void 0?`gateway ${e.method} ${e.host}${e.url} \u2192 no response: the caller went away after ${o}ms`:`gateway ${e.method} ${e.host}${e.url} \u2192 ${t} (${o}ms)`,`gateway-request:${s}`,{kind:"gateway-request",project:s,host:e.host,method:e.method,url:e.url,...t===void 0?{}:{status:t},durationMs:o,startedAt:e.startedAt,steps:e.steps,...r},{...e.flowId?{flowId:e.flowId}:{},...e.requestId?{requestId:e.requestId}:{}})},"finish"),recordEdge:m,buildStatusSnapshot:g,startFlusher:c(()=>{const e=setInterval(()=>{if($(),a.onStatus)try{a.onStatus(g())}catch{}if(a.emit)for(const s of h.values())v(s.errors?"warning":"info",`traffic ${s.host} \u2192 ${s.service}${s.version?"@"+s.version:""}: ${s.count} req`,`gateway-traffic:${s.host}->${s.service}${s.version?"@"+s.version:""}`,{kind:"gateway-traffic",host:s.host,project:s.service,caller:s.caller,version:s.version,count:s.count,avgMs:Math.round(s.totalMs/s.count),errors:s.errors});h.clear()},a.trafficFlushMs??5e3);return e.unref?.(),e},"startFlusher")}}p(createObservability,"createObservability"),c(createObservability,"createObservability");
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ServiceCtl, ServiceWake } from './types';
|
|
2
|
+
/** The longest hold: a forgotten one costs its memory for no longer than this, plus one idle window. */
|
|
3
|
+
export declare const MAX_AWAKE_SEC: number;
|
|
4
|
+
/**
|
|
5
|
+
* When the idle sweep would stop `svc` (ms): a whole window after the later of its last request and its hold's end —
|
|
6
|
+
* so never before the hold ends. `undefined` when it has no window (it never idle-stops).
|
|
7
|
+
*/
|
|
8
|
+
export declare function idleStopAt(svc: Pick<ServiceCtl, 'lastUsed' | 'awakeUntil'>, idleSec: number | undefined): number | undefined;
|
|
9
|
+
/** What `/status` and `/awake` say of a service's idle clock. `idleStopAt` only for an up tree this gateway would stop. */
|
|
10
|
+
export declare function idleFields(svc: Pick<ServiceCtl, 'state' | 'proc' | 'lastUsed' | 'awakeUntil' | 'lastIdleStop' | 'wake'>, idleSec: number | undefined, now: number): {
|
|
11
|
+
idleSec?: number;
|
|
12
|
+
idleStopAt?: number;
|
|
13
|
+
awakeUntil?: number;
|
|
14
|
+
lastIdleStop?: {
|
|
15
|
+
at: number;
|
|
16
|
+
why: string;
|
|
17
|
+
};
|
|
18
|
+
wake?: ServiceWake;
|
|
19
|
+
};
|
|
20
|
+
export interface WakeDeps {
|
|
21
|
+
/** Start it and wait until it answers — control.ts's `POST /service` start: `true` once it does. */
|
|
22
|
+
start(svc: ServiceCtl): Promise<boolean>;
|
|
23
|
+
/** warm-start.ts `availableMemory`. */
|
|
24
|
+
available(): number | undefined;
|
|
25
|
+
/** warm-start.ts `warmMinFreeBytes`. */
|
|
26
|
+
minFreeBytes: number;
|
|
27
|
+
/** Is this service still its workspace's — not unregistered, not replaced, the gateway not stopping? */
|
|
28
|
+
current(svc: ServiceCtl): boolean;
|
|
29
|
+
/** Is a stop, start or restart of it under way (what `POST /service` refuses another for)? */
|
|
30
|
+
busy(svc: ServiceCtl): boolean;
|
|
31
|
+
/** Onto the service's own bus, as the lifecycle's events go. */
|
|
32
|
+
emit(svc: ServiceCtl, severity: 'info' | 'warning', title: string, raw: Record<string, unknown>): void;
|
|
33
|
+
log(line: string): void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Start each queued service, ONE AT A TIME, each awaited, while the memory floor holds — writing how it went on its
|
|
37
|
+
* `wake` record. A service whose record a newer wake replaced is that wake's to start. Never throws.
|
|
38
|
+
*/
|
|
39
|
+
export declare function wakeServices(queued: ReadonlyArray<{
|
|
40
|
+
svc: ServiceCtl;
|
|
41
|
+
wake: ServiceWake;
|
|
42
|
+
}>, deps: WakeDeps): Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";var k=Object.defineProperty;var d=(t,e)=>k(t,"name",{value:e,configurable:!0});var p=Object.defineProperty,l=d((t,e)=>p(t,"name",{value:e,configurable:!0}),"l");Object.defineProperty(exports,"__esModule",{value:!0}),exports.MAX_AWAKE_SEC=void 0,exports.idleStopAt=idleStopAt,exports.idleFields=idleFields,exports.wakeServices=wakeServices;const warm_start_1=require("./warm-start");exports.MAX_AWAKE_SEC=14400;function idleStopAt(t,e){if(e)return Math.max(t.lastUsed||0,t.awakeUntil??0)+e*1e3}d(idleStopAt,"idleStopAt"),l(idleStopAt,"idleStopAt");function idleFields(t,e,n){const s=t.state==="up"&&t.proc?idleStopAt(t,e):void 0;return{...e?{idleSec:e}:{},...s!==void 0?{idleStopAt:s}:{},...(t.awakeUntil??0)>n?{awakeUntil:t.awakeUntil}:{},...t.lastIdleStop?{lastIdleStop:t.lastIdleStop}:{},...t.wake?{wake:t.wake}:{}}}d(idleFields,"idleFields"),l(idleFields,"idleFields");async function wakeServices(t,e){const n=l(a=>a.svc.wake===a.wake,"mine"),s=l((a,r,i)=>{a.wake={at:a.wake.at,outcome:r,...i?{reason:i}:{}},a.svc.wake=a.wake},"settle"),u=t.map(a=>({...a}));for(let a=0;a<u.length;a++){const r=u[a];if(!n(r))continue;const{svc:i}=r;if(!e.current(i)){for(const f of u.slice(a).filter(n))s(f,"failed","its workspace left the gateway before its turn");return}if(i.state==="up"){s(r,"up");continue}if(i.manualStopped){s(r,"skipped","stopped by hand while it waited its turn");continue}if(e.busy(i)){s(r,"skipped","a stop, start or restart of it was under way");continue}const c=(0,warm_start_1.belowFloor)(e.available(),e.minFreeBytes);if(c){const f=u.slice(a).filter(o=>n(o)&&o.svc.state!=="up");e.log(`[gateway] wake of ${i.wsKey} stopped: ${c} \u2014 ${f.map(o=>o.svc.project).join(", ")} start on their first request.`);for(const o of f)s(o,"deferred",c),e.emit(o.svc,"info",`wake deferred: ${o.svc.project} \u2014 ${c}`,{kind:"service-warm-deferred",project:o.svc.project,reason:c,minFreeBytes:e.minFreeBytes});return}s(r,"starting");const w=await e.start(i).catch(()=>!1);n(r)&&(w&&(i.lastUsed=Date.now()),s(r,w?"started":"failed",w?void 0:i.actionFailure??"it did not become ready"))}}d(wakeServices,"wakeServices"),l(wakeServices,"wakeServices");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ActStore } from './act';
|
|
2
2
|
import type { ServiceLayer } from './lifecycle';
|
|
3
|
-
import type { GatewayRuntime } from './types';
|
|
3
|
+
import type { GatewayRuntime, ServiceCtl } from './types';
|
|
4
4
|
import type { WorkspaceFragment } from './route-registry';
|
|
5
5
|
/** What `/list` and `/status` say about a workspace that is a worktree instance; nothing for any other. */
|
|
6
6
|
export declare function instanceSummary(f: WorkspaceFragment | undefined): {
|
|
@@ -19,6 +19,16 @@ export interface ControlServerDeps {
|
|
|
19
19
|
/** Restart one service (the svcLayer's `restartService`); `POST /restart` answers 501 without it. */
|
|
20
20
|
restartService?: ServiceLayer['restartService'];
|
|
21
21
|
ensureService?: ServiceLayer['ensureUp'];
|
|
22
|
+
/**
|
|
23
|
+
* The machine's one start at a time (warm-start.ts `createWorktreeWarmer().take`): a `POST /awake` start waits its
|
|
24
|
+
* turn behind the worktree warm starts. Without it, the starts this control plane asks for take turns among themselves.
|
|
25
|
+
*/
|
|
26
|
+
startTurn?: <T>(job: () => Promise<T>) => Promise<T>;
|
|
27
|
+
/** Onto a service's own bus, as the lifecycle's events go (lifecycle.ts `emitSvc`) — `service-awake`; none without it. */
|
|
28
|
+
emitService?: (svc: ServiceCtl, severity: 'info' | 'warning', title: string, raw: Record<string, unknown>) => void;
|
|
29
|
+
/** The memory the system can still give out (warm-start.ts `availableMemory`) and the floor a start keeps. */
|
|
30
|
+
available?: () => number | undefined;
|
|
31
|
+
minFreeBytes?: number;
|
|
22
32
|
/** Who signed in, and tickets to act as them (act.ts); `/identities` and `/act` answer 501 without it. */
|
|
23
33
|
act?: ActStore;
|
|
24
34
|
/** Hooks so the daemon can host/tear-down a workspace's dashboard around the route merge (P3d).
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
"use strict";var
|
|
2
|
-
`)}catch{}let K;if(t.graphStores){const m={root:e.root,daemonRoot:t.root,otherRoots:t.registry.keys().filter(T=>T!==e.wsKey).flatMap(T=>t.registry.get(T)?.root??[])};K=e.projectGraph===void 0?void 0:(0,guest_graph_1.admitForwardedGraph)(e.projectGraph,{...m,bytes:graphShare(e,n)}),K?.refused!==void 0&&console.warn(`[gateway] refused the project graph '${e.wsKey}' forwarded from ${e.root}: ${K.refused} \u2014 its services start through \`nx run\` until the gateway computes it`),(0,registration_guards_1.sameRoot)(e.root,t.root)||t.graphStores.set((0,guest_graph_1.createGuestGraphStore)({wsKey:e.wsKey,root:e.root,...K?{admission:K}:{},context:m}))}return t.registry.register($),await i.onRegister?.($),t.rebuildRoutes(),t.root&&!(0,registration_guards_1.sameRoot)(e.root,t.root)&&(0,guest_replay_1.recordGuest)({wsKey:e.wsKey,root:e.root,projects:r,...e.projectDependencies?{projectDependencies:e.projectDependencies}:{},daemonRoot:t.root,registeredAt:new Date().toISOString()}),{status:200,json:{ok:!0,wsKey:e.wsKey,routes:y.length,services:d.length,workspaces:t.registry.keys(),...t.graphStores?{projectGraph:graphVerdict(K)}:{}}}},"register"),S=f((e,n)=>{const c=t.registry.get(e);if(!c)return{error:`no workspace '${e}' is registered`,workspaces:t.registry.keys()};const r=c.services.find(s=>s.project===n);return r?{svc:r}:{error:`'${e}' has no service '${n}'`,services:c.services.map(s=>s.project)}},"findService"),v=f(e=>({wsKey:e.wsKey,service:e.project,state:e.state,...e.generation!==void 0?{generation:e.generation}:{},...e.restart?{restart:e.restart}:{}}),"restartView"),k=http.createServer((e,n)=>{(async()=>{try{const c=(e.url??"").split("?")[0];if(e.method==="GET"&&c==="/list")return a(n,200,{workspaces:t.registry.keys().map(r=>{const s=t.registry.get(r);return{key:r,root:s?.root,routes:s?.routes.length??0,services:s?.services.length??0,...instanceSummary(s)}})});if(e.method==="GET"&&c==="/status"){const r=(0,exports.clusterVersion)();return a(n,200,{daemon:{pid:process.pid,wsKey:t.wsKey,...r?{version:r}:{}},workspaces:t.registry.keys().map(s=>{const o=t.registry.get(s);return{key:s,root:o?.root,routes:o?.routes.length??0,services:o?.services.length??0,isDaemon:s===t.wsKey,...instanceSummary(o)}}),services:t.registry.allServices().map(s=>({project:s.project,wsKey:s.wsKey,host:s.decl.host,state:s.state,...s.generation!==void 0?{generation:s.generation}:{},...s.restart?{restart:s.restart}:{}}))})}if(e.method==="GET"&&c==="/instance-env"){const r=new URL(e.url??"/","http://control").searchParams.get("wsKey")??"",s=t.registry.get(r);return s?s.instance?a(n,200,{wsKey:r,root:s.root,...instanceSummary(s),env:s.instance.env}):a(n,404,{error:`'${r}' is not an instance`}):a(n,404,{error:`no workspace '${r}' is registered`})}if(e.method==="POST"&&c==="/instance-env"){const{value:r,bytes:s}=await readJsonBody(e,CONTROL_BODY_BYTES+guest_graph_1.MAX_FORWARDED_GRAPH_BYTES),o=await g(r,s,!0);return a(n,o.status,o.json)}if(c==="/service"&&(e.method==="GET"||e.method==="POST")){const r=e.method==="GET"?Object.fromEntries(new URL(e.url??"/","http://control").searchParams):(await readJsonBody(e)).value??{};if(typeof r.wsKey!="string"||typeof r.service!="string")return a(n,400,{error:"wsKey and service are required"});const s=S(r.wsKey,r.service);if("error"in s)return a(n,404,s);const o=s.svc,u=f(()=>{let d=(o.stoppingUntil??0)>Date.now();if(o.stoppingPid)try{process.kill(o.stoppingPid,0),d=!0}catch{o.stoppingPid=void 0}return{service:o.project,state:o.state,stopped:o.manualStopped===!0&&!d,ready:!o.manualStopped&&o.state==="up"&&o.pool.socks.length>0,busy:d||o.actionStarting===!0||!!o.rolling||!!(o.restart&&!o.restart.done),failure:o.actionFailure??(!o.manualStopped&&o.restart?.done?.ok===!1?o.restart.done.error:void 0)}},"view");if(e.method==="GET")return a(n,200,u());const p=r.action;if(!["start","stop","restart"].includes(String(p)))return a(n,400,{error:"action must be start, stop or restart"});if(u().busy&&!(p==="stop"&&o.actionStarting&&!o.rolling))return a(n,409,{error:"A service lifecycle operation is already in progress"});if(p==="stop")return!o.proc&&o.pool.socks.length&&!o.manualStopped?a(n,409,{error:"This gateway does not own the adopted service process; stop it with its owner"}):(o.manualStopped=!0,o.stoppingPid=o.proc?.pid,o.stoppingUntil=Date.now()+(o.proc?1600:0),i.killService(o,"workspace action stop"),o.state="down",a(n,202,u()));if(p==="restart"){if(!i.restartService)return a(n,501,{error:"This gateway cannot restart services"});const d=i.restartService(o,{hard:r.hard===!0,reason:"workspace action restart"});return"refused"in d?a(n,409,{error:d.refused}):a(n,202,u())}if(!i.ensureService)return a(n,501,{error:"This gateway cannot start services"});const y=o.state!=="up"&&!o.proc;return o.manualStopped=!1,o.actionFailure=void 0,o.restart=void 0,o.actionStarting=!0,i.ensureService(o).then(d=>{!d&&!o.manualStopped&&(o.actionFailure="Service did not become ready")},d=>{o.actionFailure=String(d).slice(0,500)}).finally(()=>{o.actionStarting=!1}),a(n,202,{...u(),started:y})}if(c==="/restart"&&(e.method==="POST"||e.method==="GET")){const r=e.method==="GET"?Object.fromEntries(new URL(e.url??"/","http://control").searchParams):(await readJsonBody(e)).value??{},s=typeof r.wsKey=="string"?r.wsKey:"",o=typeof r.service=="string"?r.service:"";if(!s||!o)return a(n,400,{error:"wsKey and service are required"});const u=S(s,o);if("error"in u)return a(n,404,u);const p=u.svc;if(e.method==="GET")return a(n,200,v(p));if(!i.restartService)return a(n,501,{error:"this gateway cannot restart a service"});const y=typeof r.reason=="string"&&r.reason.trim()?r.reason.trim().slice(0,200):"asked for",d=i.restartService(p,{hard:r.hard===!0,reason:y});return"refused"in d?a(n,409,{error:d.refused,...v(p)}):a(n,202,{ok:!0,already:d.already,...v(p)})}if(e.method==="GET"&&c==="/identities"||e.method==="POST"&&c==="/act"){const r=e.method==="GET"?Object.fromEntries(new URL(e.url??"/","http://control").searchParams):(await readJsonBody(e)).value??{},s=typeof r.wsKey=="string"?r.wsKey:"";if(!s)return a(n,400,{error:"wsKey is required"});if(!t.registry.get(s)&&s!==t.wsKey)return a(n,404,{error:`no workspace '${s}' is registered`,workspaces:t.registry.keys()});if(!i.act)return a(n,501,{error:"acting as a signed-in user is turned off on this gateway (LENSMCP_GW_ACT=0)"});if(e.method==="GET")return a(n,200,{wsKey:s,identities:i.act.list(s)});const o=f(d=>typeof r[d]=="string"&&r[d].trim()?r[d].trim().slice(0,256):void 0,"pick"),u=o("user"),p=o("tenant"),y=i.act.mint(s,{...u?{user:u}:{},...p?{tenant:p}:{}});return"error"in y?a(n,404,{wsKey:s,...y}):a(n,200,{wsKey:s,...y})}if(e.method==="POST"&&c==="/register"){const{value:r,bytes:s}=await readJsonBody(e,CONTROL_BODY_BYTES+guest_graph_1.MAX_FORWARDED_GRAPH_BYTES),o=await g(r,s);return a(n,o.status,o.json)}if(e.method==="POST"&&c==="/unregister"){const r=(await readJsonBody(e)).value;if(!r.wsKey)return a(n,400,{error:"wsKey is required"});const s=(0,registration_guards_1.refuseUnregistration)(t.registry,{wsKey:r.wsKey,...r.root?{root:r.root}:{}},t.wsKey);if(s)return console.warn(`[gateway] refused to unregister '${r.wsKey}'${r.root?` for ${r.root}`:""}: ${s.error}`),a(n,409,s);const o=t.registry.unregister(r.wsKey);if(o){for(const u of o.services){const p=(0,service_keys_1.serviceKeyId)(u.wsKey??r.wsKey,u.project),y=t.serviceKeys[p];y&&(delete t.serviceKeys[p],t.keyToProject.delete(y),t.keyToWorkspace?.delete(y))}for(const u of o.services)i.killService(u,"workspace unregistered");t.graphStores?.delete(o.root),(0,guest_replay_1.forgetGuest)(r.wsKey),i.onUnregister?.(o)}return t.rebuildRoutes(),a(n,200,{ok:!0,removed:!!o,workspaces:t.registry.keys()})}a(n,404,{error:"not found"})}catch(c){if(c instanceof BodyTooLarge)return e.resume(),a(n,413,{error:c.message});a(n,500,{error:c.message})}})()});return k.listen(l,()=>{try{fs.chmodSync(l,384)}catch(e){console.warn(`[gateway] could not restrict ${l} to 0600:`,e.message)}}),{socketPath:l,async replay(){const{replay:e,gone:n}=(0,guest_replay_1.guestsToReplay)(t.root,(0,guest_replay_1.readGuests)()),c={replayed:[],refused:[],gone:n.map(r=>r.wsKey)};for(const r of n)(0,guest_replay_1.forgetGuest)(r.wsKey),console.log(`[gateway] not re-hosting '${r.wsKey}': ${r.root} is no longer a workspace \u2014 forgotten`);for(const r of e)if(!t.registry.has(r.wsKey))try{const s=await g({wsKey:r.wsKey,root:r.root,projects:r.projects,...r.projectDependencies?{projectDependencies:r.projectDependencies}:{}},0);s.status===200?(c.replayed.push(r.wsKey),console.log(`[gateway] re-hosted '${r.wsKey}' (${r.root}) \u2014 registered before this daemon restarted`)):(c.refused.push(r.wsKey),console.warn(`[gateway] could not re-host '${r.wsKey}' (${r.root}): ${JSON.stringify(s.json)} \u2014 \`lensmcp gateway start\` there`))}catch(s){c.refused.push(r.wsKey),console.warn(`[gateway] could not re-host '${r.wsKey}' (${r.root}): ${s.message}`)}return c},close:f(()=>{k.close();try{fs.unlinkSync(l)}catch{}},"close")}}w(startControlServer,"startControlServer"),f(startControlServer,"startControlServer");
|
|
1
|
+
"use strict";var q=Object.defineProperty;var h=(c,t)=>q(c,"name",{value:t,configurable:!0});var x=Object.defineProperty,l=h((c,t)=>x(c,"name",{value:t,configurable:!0}),"l");Object.defineProperty(exports,"__esModule",{value:!0}),exports.clusterVersion=void 0,exports.instanceSummary=instanceSummary,exports.controlSocketPath=controlSocketPath,exports.versionAtLoad=versionAtLoad,exports.readClusterVersion=readClusterVersion,exports.ensureOwnerOnlyDir=ensureOwnerOnlyDir,exports.startControlServer=startControlServer;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),os=tslib_1.__importStar(require("node:os")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),guest_graph_1=require("./guest-graph"),guest_replay_1=require("./guest-replay"),registration_guards_1=require("./registration-guards"),scope_1=require("./scope"),awake_1=require("./awake"),warm_start_1=require("./warm-start"),service_keys_1=require("./service-keys"),workspace_log_1=require("./workspace-log"),workspace_middleware_1=require("./workspace-middleware");function instanceSummary(c){const t=c?.instance;return t?{instance:t.name,baseKey:t.baseKey,apex:t.apex,baseApex:t.baseApex,...t.frozen?{frozen:!0}:{}}:{}}h(instanceSummary,"instanceSummary"),l(instanceSummary,"instanceSummary");function controlSocketPath(){const c=process.env.LENSMCP_HOME||os.homedir();return path.join(c,".lensmcp","control.sock")}h(controlSocketPath,"controlSocketPath"),l(controlSocketPath,"controlSocketPath");const CONTROL_BODY_BYTES=1e6;class BodyTooLarge extends Error{static{h(this,"BodyTooLarge")}static{l(this,"BodyTooLarge")}constructor(t){super(`body too large (over ${t} bytes)`),this.limit=t}}exports.clusterVersion=versionAtLoad(()=>readClusterVersion(__dirname));function versionAtLoad(c){const t=c();return()=>t}h(versionAtLoad,"versionAtLoad"),l(versionAtLoad,"versionAtLoad");function readClusterVersion(c){let t=c;for(let f=0;f<6;f++){const k=path.join(t,"package.json");if(fs.existsSync(k))try{const v=JSON.parse(fs.readFileSync(k,"utf8"));if(v.name==="@lensmcp/cluster"&&typeof v.version=="string")return v.version}catch{}const i=path.dirname(t);if(i===t)break;t=i}}h(readClusterVersion,"readClusterVersion"),l(readClusterVersion,"readClusterVersion");function readJsonBody(c,t=CONTROL_BODY_BYTES){return new Promise((f,k)=>{const i=[];let v=0,_=!1;c.on("data",S=>{if(!_){if(v+=S.length,v>t){_=!0,i.length=0,k(new BodyTooLarge(t));return}i.push(S)}}),c.on("end",()=>{if(_)return;const S=Buffer.concat(i).toString("utf8");try{f({value:S?JSON.parse(S):{},bytes:v})}catch(T){k(T)}}),c.on("error",k)})}h(readJsonBody,"readJsonBody"),l(readJsonBody,"readJsonBody");function graphShare(c,t){return t-Buffer.byteLength(JSON.stringify({...c,projectGraph:null}))}h(graphShare,"graphShare"),l(graphShare,"graphShare");function graphVerdict(c){return c?c.refused===void 0?{forwarded:!0,accepted:!0}:{forwarded:!0,accepted:!1,reason:c.refused}:{forwarded:!1,accepted:!1}}h(graphVerdict,"graphVerdict"),l(graphVerdict,"graphVerdict");function ensureOwnerOnlyDir(c){try{fs.mkdirSync(c,{recursive:!0,mode:448})}catch{}try{const t=fs.lstatSync(c);return t.isDirectory()?((t.mode&63)!==0&&fs.chmodSync(c,448),!0):!1}catch{return!1}}h(ensureOwnerOnlyDir,"ensureOwnerOnlyDir"),l(ensureOwnerOnlyDir,"ensureOwnerOnlyDir");function startControlServer(c){const{rt:t}=c,f=controlSocketPath(),k=path.dirname(f);ensureOwnerOnlyDir(k)||console.warn(`[gateway] control home ${k} is not a usable owner-only directory \u2014 refusing to expose the control plane there.`);try{fs.unlinkSync(f)}catch{}const i=l((e,s,u)=>{e.writeHead(s,{"content-type":"application/json"}),e.end(JSON.stringify(u))},"send"),v=l(async(e,s,u=!1)=>{if(!e.wsKey||!e.root)return{status:400,json:{error:"wsKey and root are required"}};const r=e.projects??{},o=(0,discovery_1.projectSourceRoots)(e.root,r,e.projectDependencies),a=(0,scope_1.readLensScope)(e.root),d=a.key===e.wsKey,y=d&&a.sockNs!==e.wsKey?a.sockNs:void 0,{routes:p,services:w,instance:m}=(0,discovery_1.discoverRoutes)(e.root,r,void 0,e.wsKey,o,y,d?a.instance:void 0),b=(0,workspace_middleware_1.findGatewayMiddleware)(e.root,r),$={wsKey:e.wsKey,...y?{sockNs:y}:{},root:e.root,routes:p,services:w,...m?{instance:m}:{},...b?{middleware:b}:{}},K=(0,registration_guards_1.refuseRegistration)(t.registry,$);if(K)return console.warn(`[gateway] refused to register '${e.wsKey}' from ${e.root}: ${K.error}`),{status:409,json:K};if(u)return m?{status:200,json:{wsKey:e.wsKey,root:e.root,...instanceSummary($),env:m.env}}:{status:400,json:{error:`'${e.wsKey}' has no resolvable instance configuration`}};try{const n=(0,service_keys_1.adoptServiceKeys)(t,e.root,w,e.wsKey);n.added.length>0&&console.log(`[gateway] adopted east-west keys for ${e.wsKey}: ${n.added.join(", ")}`),n.regenerated.length>0&&console.warn(`[gateway] ${e.root} carried another workspace's east-west keys (a copied key file) \u2014 new keys for ${e.wsKey}: ${n.regenerated.join(", ")}`)}catch(n){console.error(`[gateway] could not adopt east-west keys for ${e.wsKey}: ${n.message}`)}try{(0,workspace_log_1.workspaceLogWriter)(e.root)?.(`[gateway] ${new Date().toISOString()} this workspace ('${e.wsKey}') is served by the daemon at ${t.root} (pid ${process.pid}); its gateway-level log is ${path.join(t.root,".lensmcp","gateway.log")}. Pod output for this workspace follows here. Attach live with \`lensmcp logs <service>\`.
|
|
2
|
+
`)}catch{}let j;if(t.graphStores){const n={root:e.root,daemonRoot:t.root,otherRoots:t.registry.keys().filter(g=>g!==e.wsKey).flatMap(g=>t.registry.get(g)?.root??[])};j=e.projectGraph===void 0?void 0:(0,guest_graph_1.admitForwardedGraph)(e.projectGraph,{...n,bytes:graphShare(e,s)}),j?.refused!==void 0&&console.warn(`[gateway] refused the project graph '${e.wsKey}' forwarded from ${e.root}: ${j.refused} \u2014 its services start through \`nx run\` until the gateway computes it`),(0,registration_guards_1.sameRoot)(e.root,t.root)||t.graphStores.set((0,guest_graph_1.createGuestGraphStore)({wsKey:e.wsKey,root:e.root,...j?{admission:j}:{},context:n}))}return t.registry.register($),await c.onRegister?.($),t.rebuildRoutes(),t.root&&!(0,registration_guards_1.sameRoot)(e.root,t.root)&&(0,guest_replay_1.recordGuest)({wsKey:e.wsKey,root:e.root,projects:r,...e.projectDependencies?{projectDependencies:e.projectDependencies}:{},daemonRoot:t.root,registeredAt:new Date().toISOString()}),{status:200,json:{ok:!0,wsKey:e.wsKey,routes:p.length,services:w.length,workspaces:t.registry.keys(),...t.graphStores?{projectGraph:graphVerdict(j)}:{}}}},"register"),_=l((e,s)=>{const u=t.registry.get(e);if(!u)return{error:`no workspace '${e}' is registered`,workspaces:t.registry.keys()};const r=u.services.find(o=>o.project===s);return r?{svc:r}:{error:`'${e}' has no service '${s}'`,services:u.services.map(o=>o.project)}},"findService"),S=l(e=>{let s=(e.stoppingUntil??0)>Date.now();if(e.stoppingPid)try{process.kill(e.stoppingPid,0),s=!0}catch{e.stoppingPid=void 0}return{service:e.project,state:e.state,stopped:e.manualStopped===!0&&!s,ready:!e.manualStopped&&e.state==="up"&&e.pool.socks.length>0,busy:s||e.actionStarting===!0||!!e.rolling||!!(e.restart&&!e.restart.done),failure:e.actionFailure??(!e.manualStopped&&e.restart?.done?.ok===!1?e.restart.done.error:void 0)}},"serviceView"),T=l((e,s)=>(e.manualStopped=!1,e.actionFailure=void 0,e.restart=void 0,e.actionStarting=!0,s(e).then(u=>(!u&&!e.manualStopped&&(e.actionFailure="Service did not become ready"),u),u=>(e.actionFailure=String(u).slice(0,500),!1)).finally(()=>{e.actionStarting=!1})),"startService"),O=l((e,s)=>({project:e.project,wsKey:e.wsKey,host:e.decl.host,state:e.state,...e.generation!==void 0?{generation:e.generation}:{},...e.restart?{restart:e.restart}:{},...(0,awake_1.idleFields)(e,(0,warm_start_1.serviceIdleSec)(t,e),s)}),"statusRow");let P=Promise.resolve();const A=c.startTurn??(e=>{const s=P.then(e);return P=s.catch(()=>{}),s}),E=l(e=>({wsKey:e.wsKey,service:e.project,state:e.state,...e.generation!==void 0?{generation:e.generation}:{},...e.restart?{restart:e.restart}:{}}),"restartView"),G=http.createServer((e,s)=>{(async()=>{try{const u=(e.url??"").split("?")[0];if(e.method==="GET"&&u==="/list")return i(s,200,{workspaces:t.registry.keys().map(r=>{const o=t.registry.get(r);return{key:r,root:o?.root,routes:o?.routes.length??0,services:o?.services.length??0,...instanceSummary(o)}})});if(e.method==="GET"&&u==="/status"){const r=(0,exports.clusterVersion)();return i(s,200,{daemon:{pid:process.pid,wsKey:t.wsKey,...r?{version:r}:{}},workspaces:t.registry.keys().map(o=>{const a=t.registry.get(o);return{key:o,root:a?.root,routes:a?.routes.length??0,services:a?.services.length??0,isDaemon:o===t.wsKey,...instanceSummary(a)}}),services:t.registry.allServices().map(o=>O(o,Date.now()))})}if(e.method==="GET"&&u==="/instance-env"){const r=new URL(e.url??"/","http://control").searchParams.get("wsKey")??"",o=t.registry.get(r);return o?o.instance?i(s,200,{wsKey:r,root:o.root,...instanceSummary(o),env:o.instance.env}):i(s,404,{error:`'${r}' is not an instance`}):i(s,404,{error:`no workspace '${r}' is registered`})}if(e.method==="POST"&&u==="/instance-env"){const{value:r,bytes:o}=await readJsonBody(e,CONTROL_BODY_BYTES+guest_graph_1.MAX_FORWARDED_GRAPH_BYTES),a=await v(r,o,!0);return i(s,a.status,a.json)}if(u==="/service"&&(e.method==="GET"||e.method==="POST")){const r=e.method==="GET"?Object.fromEntries(new URL(e.url??"/","http://control").searchParams):(await readJsonBody(e)).value??{};if(typeof r.wsKey!="string"||typeof r.service!="string")return i(s,400,{error:"wsKey and service are required"});const o=_(r.wsKey,r.service);if("error"in o)return i(s,404,o);const a=o.svc,d=l(()=>S(a),"view");if(e.method==="GET")return i(s,200,d());const y=r.action;if(!["start","stop","restart"].includes(String(y)))return i(s,400,{error:"action must be start, stop or restart"});if(d().busy&&!(y==="stop"&&a.actionStarting&&!a.rolling))return i(s,409,{error:"A service lifecycle operation is already in progress"});if(y==="stop")return!a.proc&&a.pool.socks.length&&!a.manualStopped?i(s,409,{error:"This gateway does not own the adopted service process; stop it with its owner"}):(a.manualStopped=!0,a.stoppingPid=a.proc?.pid,a.stoppingUntil=Date.now()+(a.proc?1600:0),c.killService(a,"workspace action stop"),a.state="down",i(s,202,d()));if(y==="restart"){if(!c.restartService)return i(s,501,{error:"This gateway cannot restart services"});const w=c.restartService(a,{hard:r.hard===!0,reason:"workspace action restart"});return"refused"in w?i(s,409,{error:w.refused}):i(s,202,d())}if(!c.ensureService)return i(s,501,{error:"This gateway cannot start services"});const p=a.state!=="up"&&!a.proc;return T(a,c.ensureService),i(s,202,{...d(),started:p})}if(e.method==="POST"&&u==="/awake"){const r=(await readJsonBody(e)).value??{},o=typeof r.wsKey=="string"?r.wsKey:"";if(!o)return i(s,400,{error:"wsKey is required"});const a=typeof r.forSec=="number"?r.forSec:NaN;if(!Number.isFinite(a)||a<0)return i(s,400,{error:"forSec is required: the seconds to hold them awake, 0 to release the hold"});if(a>awake_1.MAX_AWAKE_SEC)return i(s,400,{error:`forSec ${a} is over the ${awake_1.MAX_AWAKE_SEC} s (4 h) a hold may last \u2014 ask again before it ends to keep them longer`,maxSec:awake_1.MAX_AWAKE_SEC});const d=r.services;if(d!==void 0&&(!Array.isArray(d)||d.length===0||!d.every(n=>typeof n=="string"&&n.length>0)))return i(s,400,{error:"services, when given, names one service or more"});const y=t.registry.get(o);if(!y)return i(s,404,{error:`no workspace '${o}' is registered`,workspaces:t.registry.keys()});const p=[];for(const n of d??[]){const g=_(o,n);if("error"in g)return i(s,404,g);p.includes(g.svc)||p.push(g.svc)}const w=p.length>0;w||p.push(...y.services);const m=Date.now();if(a===0){for(const n of p)(n.awakeUntil??0)<=m||(n.awakeUntil=m,c.emitService?.(n,"info",`awake hold released: ${n.project}`,{kind:"service-awake",project:n.project,until:null,released:!0}));return i(s,202,{ok:!0,wsKey:o,forSec:0,released:!0,services:p.map(n=>O(n,m))})}const b=m+Math.round(a*1e3),$=[],K=l((n,g)=>({at:m,outcome:n,...g?{reason:g}:{}}),"asked");for(const n of p)n.awakeUntil=Math.max(n.awakeUntil??0,b),n.state==="up"?n.wake=K("up"):S(n).busy?n.wake=K("skipped","a stop, start or restart of it is under way \u2014 `lensmcp instance status` shows how it ends"):n.manualStopped&&!w?n.wake=K("skipped","stopped by hand \u2014 name it to start it"):c.ensureService?(n.manualStopped=!1,n.wake=K("queued"),$.push({svc:n,wake:n.wake})):n.wake=K("failed","this gateway cannot start services"),c.emitService?.(n,"info",`awake until ${new Date(n.awakeUntil).toISOString()}: ${n.project}`,{kind:"service-awake",project:n.project,until:n.awakeUntil,forSec:a,wake:n.wake.outcome});const j=c.ensureService;return $.length>0&&j&&A(()=>(0,awake_1.wakeServices)($,{start:l(n=>T(n,j),"start"),available:c.available??(()=>(0,warm_start_1.availableMemory)()),minFreeBytes:c.minFreeBytes??(0,warm_start_1.warmMinFreeBytes)(),current:l(n=>!t.stopped?.()&&!!t.registry.get(n.wsKey)?.services.includes(n),"current"),busy:l(n=>S(n).busy,"busy"),emit:l((n,g,D,R)=>c.emitService?.(n,g,D,R),"emit"),log:l(n=>console.log(n),"log")})).catch(n=>console.error(`[gateway] wake of ${o} failed:`,n)),i(s,202,{ok:!0,wsKey:o,forSec:a,until:b,services:p.map(n=>O(n,m))})}if(u==="/restart"&&(e.method==="POST"||e.method==="GET")){const r=e.method==="GET"?Object.fromEntries(new URL(e.url??"/","http://control").searchParams):(await readJsonBody(e)).value??{},o=typeof r.wsKey=="string"?r.wsKey:"",a=typeof r.service=="string"?r.service:"";if(!o||!a)return i(s,400,{error:"wsKey and service are required"});const d=_(o,a);if("error"in d)return i(s,404,d);const y=d.svc;if(e.method==="GET")return i(s,200,E(y));if(!c.restartService)return i(s,501,{error:"this gateway cannot restart a service"});const p=typeof r.reason=="string"&&r.reason.trim()?r.reason.trim().slice(0,200):"asked for",w=c.restartService(y,{hard:r.hard===!0,reason:p});return"refused"in w?i(s,409,{error:w.refused,...E(y)}):i(s,202,{ok:!0,already:w.already,...E(y)})}if(e.method==="GET"&&u==="/identities"||e.method==="POST"&&u==="/act"){const r=e.method==="GET"?Object.fromEntries(new URL(e.url??"/","http://control").searchParams):(await readJsonBody(e)).value??{},o=typeof r.wsKey=="string"?r.wsKey:"";if(!o)return i(s,400,{error:"wsKey is required"});if(!t.registry.get(o)&&o!==t.wsKey)return i(s,404,{error:`no workspace '${o}' is registered`,workspaces:t.registry.keys()});if(!c.act)return i(s,501,{error:"acting as a signed-in user is turned off on this gateway (LENSMCP_GW_ACT=0)"});if(e.method==="GET")return i(s,200,{wsKey:o,identities:c.act.list(o)});const a=l(w=>typeof r[w]=="string"&&r[w].trim()?r[w].trim().slice(0,256):void 0,"pick"),d=a("user"),y=a("tenant"),p=c.act.mint(o,{...d?{user:d}:{},...y?{tenant:y}:{}});return"error"in p?i(s,404,{wsKey:o,...p}):i(s,200,{wsKey:o,...p})}if(e.method==="POST"&&u==="/register"){const{value:r,bytes:o}=await readJsonBody(e,CONTROL_BODY_BYTES+guest_graph_1.MAX_FORWARDED_GRAPH_BYTES),a=await v(r,o);return i(s,a.status,a.json)}if(e.method==="POST"&&u==="/unregister"){const r=(await readJsonBody(e)).value;if(!r.wsKey)return i(s,400,{error:"wsKey is required"});const o=(0,registration_guards_1.refuseUnregistration)(t.registry,{wsKey:r.wsKey,...r.root?{root:r.root}:{}},t.wsKey);if(o)return console.warn(`[gateway] refused to unregister '${r.wsKey}'${r.root?` for ${r.root}`:""}: ${o.error}`),i(s,409,o);const a=t.registry.unregister(r.wsKey);if(a){for(const d of a.services){const y=(0,service_keys_1.serviceKeyId)(d.wsKey??r.wsKey,d.project),p=t.serviceKeys[y];p&&(delete t.serviceKeys[y],t.keyToProject.delete(p),t.keyToWorkspace?.delete(p))}for(const d of a.services)c.killService(d,"workspace unregistered");t.graphStores?.delete(a.root),(0,guest_replay_1.forgetGuest)(r.wsKey),c.onUnregister?.(a)}return t.rebuildRoutes(),i(s,200,{ok:!0,removed:!!a,workspaces:t.registry.keys()})}i(s,404,{error:"not found"})}catch(u){if(u instanceof BodyTooLarge)return e.resume(),i(s,413,{error:u.message});i(s,500,{error:u.message})}})()});return G.listen(f,()=>{try{fs.chmodSync(f,384)}catch(e){console.warn(`[gateway] could not restrict ${f} to 0600:`,e.message)}}),{socketPath:f,async replay(){const{replay:e,gone:s}=(0,guest_replay_1.guestsToReplay)(t.root,(0,guest_replay_1.readGuests)()),u={replayed:[],refused:[],gone:s.map(r=>r.wsKey)};for(const r of s)(0,guest_replay_1.forgetGuest)(r.wsKey),console.log(`[gateway] not re-hosting '${r.wsKey}': ${r.root} is no longer a workspace \u2014 forgotten`);for(const r of e)if(!t.registry.has(r.wsKey))try{const o=await v({wsKey:r.wsKey,root:r.root,projects:r.projects,...r.projectDependencies?{projectDependencies:r.projectDependencies}:{}},0);o.status===200?(u.replayed.push(r.wsKey),console.log(`[gateway] re-hosted '${r.wsKey}' (${r.root}) \u2014 registered before this daemon restarted`)):(u.refused.push(r.wsKey),console.warn(`[gateway] could not re-host '${r.wsKey}' (${r.root}): ${JSON.stringify(o.json)} \u2014 \`lensmcp gateway start\` there`))}catch(o){u.refused.push(r.wsKey),console.warn(`[gateway] could not re-host '${r.wsKey}' (${r.root}): ${o.message}`)}return u},close:l(()=>{G.close();try{fs.unlinkSync(f)}catch{}},"close")}}h(startControlServer,"startControlServer"),l(startControlServer,"startControlServer");
|
|
@@ -8,7 +8,7 @@ import * as net from 'node:net';
|
|
|
8
8
|
import { probePodAlive } from './pod-probe';
|
|
9
9
|
import { type SharedBuildRelay } from './shared-build';
|
|
10
10
|
import type { GatewayRuntime, LensWatcherHealth, PodWatcherTestimony, PodWatcherVerdict, ServiceCtl, ServiceRestart, SockPool, SourceSetChange } from './types';
|
|
11
|
-
import type { Observability } from './observability';
|
|
11
|
+
import type { Observability, Severity } from './observability';
|
|
12
12
|
import { type PsRow } from './orphan-reaper';
|
|
13
13
|
/** Bounded, async, non-blocking walk of the workspace source tree → a signature of the file SET
|
|
14
14
|
* (`<count>:<xor-of-path-hashes>`). Skips node_modules/dist/build/dotdirs + `*-devserver` sock dirs + every
|
|
@@ -277,6 +277,9 @@ export interface ServiceLayer {
|
|
|
277
277
|
* Idempotent (a no-op once `state === 'up'`), so the hot path can call it per
|
|
278
278
|
* request. Returns true only on the transition. See the implementation. */
|
|
279
279
|
markUp(svc: ServiceCtl, how: 'adopted' | 'observed'): boolean;
|
|
280
|
+
/** Onto the service's OWN workspace's bus — the lifecycle's events, and those the control plane raises for a service
|
|
281
|
+
* (control.ts `POST /awake`). `fingerprint` is the flood bucket: identity goes in `raw`. */
|
|
282
|
+
emitSvc(svc: ServiceCtl, severity: Severity, title: string, fingerprint: string, raw: Record<string, unknown>): void;
|
|
280
283
|
/** Start the idle sweeper; returns the interval (already `.unref()`'d). */
|
|
281
284
|
startSweeper(): NodeJS.Timeout;
|
|
282
285
|
/** Rolling recycle: standby generation first, retire the old once the new answers (issues/closed/dev-gateway-09). */
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"use strict";var it=Object.defineProperty;var m=(t,c)=>it(t,"name",{value:c,configurable:!0});var at=Object.defineProperty,a=m((t,c)=>at(t,"name",{value:c,configurable:!0}),"a");Object.defineProperty(exports,"__esModule",{value:!0}),exports.scaleTarget=scaleTarget,exports.isLoopbackHostname=isLoopbackHostname,exports.isOwnRunnerCommand=isOwnRunnerCommand,exports.isRunnerOf=isRunnerOf,exports.isTestSourcePath=isTestSourcePath,exports.sourceScanPass=sourceScanPass,exports.sourceSetSnapshot=sourceSetSnapshot,exports.sourceSetSignature=sourceSetSignature,exports.diffSourceSets=diffSourceSets,exports.sourceChangeWindowMs=sourceChangeWindowMs,exports.sourceScopeDirs=sourceScopeDirs,exports.podRecycleReason=podRecycleReason,exports.lensFrontendStale=lensFrontendStale,exports.lensWatcherVerdict=lensWatcherVerdict,exports.podWatcherVerdict=podWatcherVerdict,exports.lensChildWedged=lensChildWedged,exports.isPortConflictExit=isPortConflictExit,exports.podChangeClock=podChangeClock,exports.createServiceLayer=createServiceLayer,exports.chooseStalePodKillTarget=chooseStalePodKillTarget;const tslib_1=require("tslib"),node_child_process_1=require("node:child_process"),node_crypto_1=require("node:crypto"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),net=tslib_1.__importStar(require("node:net")),path=tslib_1.__importStar(require("node:path")),node_util_1=require("node:util"),discovery_1=require("./discovery"),pod_sock_1=require("../../../pod-sock"),scope_1=require("./scope"),pod_env_1=require("./pod-env"),pod_probe_1=require("./pod-probe"),service_keys_1=require("./service-keys"),workspace_log_1=require("./workspace-log"),own_log_1=require("./own-log"),inspector_secret_1=require("../../../inspector-secret"),pod_output_1=require("./pod-output"),serve_launch_1=require("./serve-launch"),shared_build_1=require("./shared-build"),warm_start_1=require("./warm-start"),protocol_1=require("../../../shared-build/protocol"),types_1=require("./types"),exit_reason_1=require("../../../exit-reason"),handoff_retire_1=require("../../../handoff-retire"),process_name_1=require("../../../process-name"),orphan_reaper_1=require("./orphan-reaper");function eastWestEnv(t,c){const l={},p=t.serviceKeys[(0,service_keys_1.serviceKeyId)(c.wsKey,c.project)];if(p&&(l.LENSMCP_SERVICE_KEY=p),t.https){const f=require("../../../basic-ssl").caCertPath();fs.existsSync(f)&&(l.NODE_EXTRA_CA_CERTS=f)}return l}m(eastWestEnv,"eastWestEnv"),a(eastWestEnv,"eastWestEnv");const sleep=a(t=>new Promise(c=>setTimeout(c,t)),"sleep"),SRC_EXCLUDE=new Set(["node_modules","dist","tmp","coverage","build","out","target"]),SRC_EXT=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".css",".scss",".sass",".less",".json",".html",".htm",".vue",".svelte",".mdx"]),SCAN_FILE_CAP=1e5;function fnv1a(t){let c=2166136261;for(let l=0;l<t.length;l+=1)c^=t.charCodeAt(l),c=Math.imul(c,16777619);return c>>>0}m(fnv1a,"fnv1a"),a(fnv1a,"fnv1a");const execFileP=(0,node_util_1.promisify)(node_child_process_1.execFile);async function gitIgnoredPaths(t){try{const{stdout:c}=await execFileP("git",["ls-files","--others","--directory","--ignored","--exclude-standard","-z"],{cwd:t,maxBuffer:33554432,timeout:1e4,windowsHide:!0});return new Set(c.split("\0").filter(Boolean))}catch{return new Set}}m(gitIgnoredPaths,"gitIgnoredPaths"),a(gitIgnoredPaths,"gitIgnoredPaths");function scaleTarget(t,c=fs.existsSync){if(t.decl.adminUrl)return{url:t.decl.adminUrl};const l=path.join(t.pool.dir,"parent.sock");return c(l)?{socketPath:l}:t.decl.port?{url:`http://localhost:${t.decl.port}`}:void 0}m(scaleTarget,"scaleTarget"),a(scaleTarget,"scaleTarget");function isLoopbackHostname(t){const c=t.toLowerCase().replace(/^\[(.*)\]$/,"$1").replace(/\.$/,"");return c==="localhost"||c.endsWith(".localhost")?!0:net.isIPv4(c)?c.split(".")[0]==="127":net.isIPv6(c)&&new URL(`http://[${c}]/`).hostname==="[::1]"}m(isLoopbackHostname,"isLoopbackHostname"),a(isLoopbackHostname,"isLoopbackHostname");function postScale(t,c,l){let p;if("socketPath"in t)p=http.request({socketPath:t.socketPath,path:"/webpack/scale",method:"POST",headers:{"content-type":"application/json"}},f=>f.resume());else{const f=t.url.startsWith("https"),T=f?require("node:https"):http,_={method:"POST"};f&&isLoopbackHostname(new URL(t.url).hostname)&&(_.rejectUnauthorized=!1),l&&(_.lookup=l),p=T.request(`${t.url}/webpack/scale`,_,A=>A.resume())}return p.on("error",()=>{}),p.end(JSON.stringify({pods:c})),p}m(postScale,"postScale"),a(postScale,"postScale");function isOwnRunnerCommand(t,c){return(0,serve_launch_1.isServeRunnerCommand)(t)?(0,serve_launch_1.isServeRunnerCommandOf)(t,c):t.includes("/node_modules/nx/")?t.includes(`${c.replace(/\/+$/,"")}/node_modules/nx/`):!0}m(isOwnRunnerCommand,"isOwnRunnerCommand"),a(isOwnRunnerCommand,"isOwnRunnerCommand");function isRunnerOf(t,c,l){const p=c.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return!new RegExp(`(^|\\s)run ${p}(:|\\s|$)`).test(t)||!(0,serve_launch_1.isServeRunnerCommand)(t)&&!/\bnx(\.js)?\b/.test(t)?!1:isOwnRunnerCommand(t,l)}m(isRunnerOf,"isRunnerOf"),a(isRunnerOf,"isRunnerOf");const TEST_FILE_RE=/\.(spec|test|e2e|e2e-spec|stories)\.[cm]?[jt]sx?$/,TEST_DIR_SEGMENTS=new Set(["__tests__","__mocks__","__snapshots__"]);function isTestSourcePath(t){const c=t.split("/"),l=c[c.length-1]??"";if(TEST_FILE_RE.test(l))return!0;for(let p=0;p<c.length-1;p+=1)if(TEST_DIR_SEGMENTS.has(c[p]))return!0;return!1}m(isTestSourcePath,"isTestSourcePath"),a(isTestSourcePath,"isTestSourcePath");function sourceScanPass(t,c,l){const p=l?.ignoreTests??!0;let f;const T=new Map,_=`tree:${path.resolve(t)}`,A=a(y=>path.relative(t,y).split(path.sep).join("/"),"rel"),C=a(async(y,$)=>{const E=await(f??=gitIgnoredPaths(t)),b=[],F=new Set;let D=!1;const H=a(async(te,S)=>{if(b.length>=SCAN_FILE_CAP){D=!0;return}let O;try{O=await fs.promises.readdir(te,{withFileTypes:!0})}catch{return}S&&F.add(A(te));for(const z of O){if(b.length>=SCAN_FILE_CAP){D=!0;return}const W=z.name,re=path.join(te,W);if(z.isDirectory()){if(!S||W.startsWith(".")||SRC_EXCLUDE.has(W)||c?.has(W)||W.endsWith("-devserver")||E.has(`${A(re)}/`))continue;await H(re,!0)}else if(z.isFile()){const oe=W.lastIndexOf(".");if(oe<=0||!SRC_EXT.has(W.slice(oe)))continue;const J=A(re);if(E.has(J)||p&&isTestSourcePath(J))continue;b.push(J)}}},"walk");return await H(y,$),{paths:b,entered:F,truncated:D}},"walkDir"),L=a((y,$)=>{let E=T.get(y);return E||(E=$(),T.set(y,E)),E},"memo"),ie=a(async()=>{const y=T.get(_);if(!y)return;const $=await y;return $.truncated?void 0:$},"wideWalk"),B=new Set,le=a(()=>L(`files:${path.resolve(t)}`,async()=>{const y=await ie();return y?{paths:y.paths.filter($=>!$.includes("/")),entered:B,truncated:!1}:C(t,!1)}),"rootFiles"),de=a(y=>L(`tree:${path.resolve(y)}`,async()=>{const $=await ie(),E=A(y);if($?.entered.has(E)){const b=`${E}/`;return{paths:E===""?$.paths:$.paths.filter(F=>F.startsWith(b)),entered:B,truncated:!1}}return C(y,!0)}),"subtree"),X=a(y=>{const $=new Set;let E=0,b=y.some(D=>D.truncated);const F=a(D=>$.has(D)?!0:$.size>=SCAN_FILE_CAP?(b=!0,!1):($.add(D),E^=fnv1a(D),!0),"add");for(const D of y)if(!D.paths.every(F))break;return{signature:`${$.size}:${(E>>>0).toString(36)}`,paths:$,truncated:b}},"compose");return{async snapshot(y){if(!y||y.length===0)return X([await L(_,()=>C(t,!0))]);const $=[await le()];for(const E of y)$.push(await de(E));return X($)}}}m(sourceScanPass,"sourceScanPass"),a(sourceScanPass,"sourceScanPass");async function sourceSetSnapshot(t,c,l,p){return sourceScanPass(t,l,p).snapshot(c)}m(sourceSetSnapshot,"sourceSetSnapshot"),a(sourceSetSnapshot,"sourceSetSnapshot");async function sourceSetSignature(t,c,l,p){return(await sourceSetSnapshot(t,c,l,p)).signature}m(sourceSetSignature,"sourceSetSignature"),a(sourceSetSignature,"sourceSetSignature");const SOURCE_DIFF_PATH_CAP=128;function diffSourceSets(t,c,l,p){if(t.paths.size===c.paths.size){let A=!0;for(const C of t.paths)if(!c.paths.has(C)){A=!1;break}if(A)return null}const f=[],T=[];let _=t.truncated||c.truncated;for(const A of c.paths)t.paths.has(A)||(f.length+T.length<SOURCE_DIFF_PATH_CAP?f.push(A):_=!0);for(const A of t.paths)c.paths.has(A)||(f.length+T.length<SOURCE_DIFF_PATH_CAP?T.push(A):_=!0);return{changedAt:l,added:f,removed:T,truncated:_,...p!==void 0?{windowMs:p}:{}}}m(diffSourceSets,"diffSourceSets"),a(diffSourceSets,"diffSourceSets");function sourceChangeWindowMs(t,c,l,p){const f=c&&c>0?t-c:l;return Math.max(f,l)+p}m(sourceChangeWindowMs,"sourceChangeWindowMs"),a(sourceChangeWindowMs,"sourceChangeWindowMs");function sourceScopeDirs(t,c,l,p,f){const T=a(C=>{const L=path.relative(t,C).split(path.sep).filter(Boolean);return L.length===0||L[0].startsWith("..")?"":L[0]},"bucketOf")(c);if(!T)return[];const _=new Set(l.filter(Boolean)),A=new Set([T]);for(const C of p)C.startsWith(".")||SRC_EXCLUDE.has(C)||f?.has(C)||C.endsWith("-devserver")||_.has(C)||A.add(C);return[...A].sort().map(C=>path.join(t,C))}m(sourceScopeDirs,"sourceScopeDirs"),a(sourceScopeDirs,"sourceScopeDirs");function podRecycleReason(t,c,l,p){return!t.proc||t.state!=="up"||t.spawnAt==null||t.inflight>0||(t.activeWork??0)>0||c-t.lastUsed<p.settleMs||t.lastStaleRecycleAt!=null&&c-t.lastStaleRecycleAt<p.cooldownMs?null:l>0&&l-t.spawnAt>p.graceMs?"stale-source-set":p.maxAgeMs>0&&c-t.spawnAt>p.maxAgeMs?"max-age":null}m(podRecycleReason,"podRecycleReason"),a(podRecycleReason,"podRecycleReason");function lensFrontendStale(t,c,l,p,f){return!(p-c<f.cooldownMs||l<=0||l-t<=f.graceMs||p-l<f.settleMs)}m(lensFrontendStale,"lensFrontendStale"),a(lensFrontendStale,"lensFrontendStale");function lensWatcherVerdict(t,c,l,p=[]){if(!t)return"unknown";if("schemaVersion"in t&&t.schemaVersion>=2&&p.length>0){const f=c-l,T=new Set(t.recentEvents.filter(_=>_.at>=f).map(_=>path.resolve(_.path)));return p.every(_=>T.has(path.resolve(_)))?"heard":"missed"}return t.lastFsSetEventAt>=c-l?"heard":"missed"}m(lensWatcherVerdict,"lensWatcherVerdict"),a(lensWatcherVerdict,"lensWatcherVerdict");function podWatcherVerdict(t,c,l){if(!t)return"unknown";if(t.degraded)return"degraded";const p=c-l;return t.lastWatchRunAt>=p||t.lastBuildAt>=p?"heard":"silent"}m(podWatcherVerdict,"podWatcherVerdict"),a(podWatcherVerdict,"podWatcherVerdict");function lensChildWedged(t,c,l,p){const f=t.hasListened?p.graceMs:Math.max(p.graceMs,p.neverListenedGraceMs);return l-t.spawnedAt<f||l-c<p.cooldownMs||t.probeFails<Math.max(2,p.failureThreshold)?null:"wedged-no-listener"}m(lensChildWedged,"lensChildWedged"),a(lensChildWedged,"lensChildWedged");function isPortConflictExit(t,c){return t.signal||t.code===null||t.code===0||t.ranMs>c.fastFailMs?!1:t.portHeld}m(isPortConflictExit,"isPortConflictExit"),a(isPortConflictExit,"isPortConflictExit");function podChangeClock(t,c){const l=t.sourceSetChangedFor?.get(c);if(l!==void 0){const p=t.sourceSetChangesFor?.get(c);return{at:l,...p?{change:p}:{},scope:"project"}}return t.sourceSetScoped?.has(c)?{at:0,scope:"none"}:{at:t.sourceSetChangedAt??0,...t.sourceSetChange?{change:t.sourceSetChange}:{},scope:"workspace"}}m(podChangeClock,"podChangeClock"),a(podChangeClock,"podChangeClock");const changeEvidence=a(t=>{if(!t)return"no path evidence";const c=[...t.added.map(l=>`+${l}`),...t.removed.map(l=>`-${l}`)];return c.length?`${c.slice(0,3).join(", ")}${c.length>3?` (+${c.length-3} more)`:""}`:"no path evidence"},"changeEvidence");function createServiceLayer(t,c,l={}){const p=l.spawnProcess??node_child_process_1.spawn,f=l.probeAlive??pod_probe_1.probePodAlive,T=l.killTree??((e,r="SIGTERM")=>{try{process.kill(-e.pid,r)}catch{try{e.kill(r)}catch{}}}),_=l.sharedBuild??(0,shared_build_1.createSharedBuildRelay)(t),A=a((e,r="SIGTERM")=>{_.detach(e),T(e,r)},"killTree"),C=l.handoffDrainMs??(0,handoff_retire_1.handoffDrainBoundMs)(),L=l.treeAlive??(e=>{if(!e.pid||!(e instanceof node_child_process_1.ChildProcess))return!1;try{return process.kill(-e.pid,0),!0}catch(r){return r.code==="EPERM"}}),ie=l.retirePollMs??1e3,B=l.rollReadyMs??types_1.POD_ROLL_READY_MS,le=l.rollPollMs??750,de=l.maxConcurrentRolls??types_1.POD_MAX_CONCURRENT_ROLLS,X=de>0?de:Number.POSITIVE_INFINITY,y=l.maxConcurrentColdStarts??types_1.MAX_COLD_STARTS,$=y>0?y:Number.POSITIVE_INFINITY;let E=0;const b=[],F=a(()=>E<$?(E+=1,Promise.resolve()):new Promise(e=>{b.push(()=>{E+=1,e()})}),"acquireColdSlot"),D=a(()=>{E-=1,b.shift()?.()},"releaseColdSlot"),{emit:H}=c,te=a(e=>e.wsKey===t.wsKey?void 0:path.join(e.root,".lensmcp","events.jsonl"),"serviceBus"),S=a((e,r,o,s,i)=>H(r,o,s,{wsKey:e.wsKey,...i},void 0,te(e)),"emitSvc"),O=a(e=>(e.scannedAt=0,(0,discovery_1.pickSock)(e,t.scanTtlMs)?e.socks.length:0),"scanNow"),z=a((e,r)=>{let o;try{o=(0,node_child_process_1.execFileSync)("ps",["ax","-ww","-o","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3})}catch{return 0}const s=e.proc?.pid;let i=0;for(const n of o.split(`
|
|
2
|
-
`)){const d=n.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);if(!d)continue;const u=Number(d[1]),
|
|
3
|
-
`)){const R=(0,pod_output_1.classifyPodLine)(g),{clean:M,broken:G,healthy:ne}=R,h=M;if(G){e.lastErrors.push(M),e.lastErrors.length>12&&e.lastErrors.shift(),e.buildFailed=!0;const x=Date.now();(M!==e.lastErrorEmitSig||x-(e.lastErrorEmitAt??0)>=types_1.SERVICE_ERROR_THROTTLE_MS)&&(e.lastErrorEmitSig=M,e.lastErrorEmitAt=x,S(e,"error",`service error: ${e.project}`,`cluster-service:${e.project}`,{kind:"service-error",project:e.project,line:M,fault:R.fault}))}ne&&(e.buildFailed=!1);const P=(e.faults??=new pod_output_1.PodFaults).observe(R);if(P){e.lastErrorEmitSig=void 0;const x=P.open.length===0;S(e,"info",`${x?"service recovered":`${P.closed.join(" + ")} recovered`}: ${e.project}`,`cluster-service:${e.project}`,{kind:x?"service-recovered":"service-fault-closed",project:e.project,line:M,closed:P.closed,open:P.open})}!e.lockBlockedAt&&/Waiting for .+ in another nx process/.test(h)&&(e.lockBlockedAt=Date.now(),console.log(`[gateway] ${s}: spawn blocked by a stale nx runner \u2014 breaking the lock + respawning`),S(e,"warning",`nx task lock: ${e.project} blocked by a stale runner \u2014 auto-breaking`,`cluster-service:${e.project}`,{kind:"nx-lock-break",project:e.project}),setTimeout(()=>{t.stopped()||!e.lockBlockedAt||(V(e,"parked on a foreign nx task lock \u2014 recycling",{announce:!1}),z(e,s),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&Y(e)},1500).unref?.())},250).unref?.())}},"sniff"),w=e.wsKey&&e.wsKey!==t.wsKey&&e.root?(0,workspace_log_1.workspaceLogWriter)(e.root):void 0,k=a(j=>{const v=(0,own_log_1.linePrefixer)(e.project);return g=>{if(t.stopped())return;const R=(0,inspector_secret_1.redactInspectorUrls)(g.toString());u(R);const M=v(R);j.write(M),w?.(M)}},"relay");return n.stdout?.on("data",k(process.stdout)),n.stderr?.on("data",k(process.stderr)),n.on("exit",(j,v)=>{if(e.proc!==n||(e.proc=void 0,e.state="down",t.stopped()))return;const g=(0,exit_reason_1.describeExit)(j,v);if(console.log(`[gateway] ${s} exited (${j??"no code"}${v?`, ${v}`:""}${e.inflight?`, ${e.inflight} request(s) in flight`:""})${g?` \u2014 ${g}`:""}`),S(e,j===0?"info":"warning",`service down: ${e.project} (exit ${j})`,`cluster-service:${e.project}`,{kind:"service-down",project:e.project,code:j,...v?{signal:v}:{}}),j===0||e.buildFailed)return;const R=Date.now();if(e.restarts=(e.restarts??[]).filter(G=>R-G<types_1.SERVICE_RESTART_WINDOW_MS),e.restarts.length>=types_1.SERVICE_MAX_RESTARTS){S(e,"error",`service crash-looping: ${e.project} \u2014 ${types_1.SERVICE_MAX_RESTARTS}+ restarts/min, not auto-respawning (the next request retries)`,`cluster-service:${e.project}`,{kind:"service-crash-loop",project:e.project});return}e.restarts.push(R);const M=Math.min(8e3,500*2**(e.restarts.length-1));setTimeout(()=>{t.stopped()||e.proc||e.state!=="down"||(console.log(`[gateway] self-heal: respawning ${e.project} (restart ${e.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`),S(e,"info",`self-heal: respawning ${e.project}`,`cluster-service:${e.project}`,{kind:"service-respawn",project:e.project,attempt:e.restarts.length}),Y(e))},M)}),n},"launchServiceProcess"),Y=a(e=>{if(t.stopped()||e.manualStopped)return;const r=`${e.project}:${e.decl.serveTarget??"serve-hmr"}`,o=Math.max(1,e.decl.minPods??1);l.reapForeign!==!1&&z(e,r),e.lockBlockedAt=void 0,console.log(`[gateway] starting ${r} (pods=${o})\u2026`),S(e,"info",`service starting: ${e.project} (${o} pod${o>1?"s":""})`,`cluster-service:${e.project}`,{kind:"service-starting",project:e.project,pods:o}),e.state="starting",e.spawnAt=Date.now(),e.generation=(e.generation??0)+1,e.proc=J(e,e.generation,"active")},"spawnService"),be=a(async e=>{const r=e.pool.socks[0];if(!r)return;const o=await(0,pod_probe_1.probePodAlive)(r,void 0,t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)]);o&&(e.activeWork=o.activeWork,o.watcher&&(e.watcher=o.watcher))},"refreshActiveWork"),Le=a(e=>{try{const r=Number(fs.readFileSync(`${e}.owner`,"utf8").trim());return Number.isInteger(r)&&r>1?r:void 0}catch{return}},"readOwnerSidecar"),Ie=a(async e=>{const r=t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)];if(!r)return[];const o=(0,node_crypto_1.createHash)("sha256").update(r).digest("hex").slice(0,8),s=[];for(const i of e.pool.socks){const n=await(0,pod_probe_1.probePodAlive)(i,void 0,r);n&&(!n.keyFingerprint||n.keyFingerprint!==o)&&s.push({sock:i,pid:n.pid})}return s},"staleAdoptedSocks"),Oe=a((e,r)=>{const o=r.map(s=>s.sock);for(const{sock:s,pid:i}of r){const n=chooseStalePodKillTarget(i,Le(s),process.pid);if(n!==void 0)try{process.kill(n,"SIGTERM")}catch{}try{fs.unlinkSync(s)}catch{}try{fs.unlinkSync(`${s}.owner`)}catch{}}e.pool.socks=e.pool.socks.filter(s=>!o.includes(s)),e.pool.idx=-1,e.pool.scannedAt=0},"evictStaleAdopted"),pe=a((e,r)=>{if(e.state==="up")return!1;e.state="up",e.wedgeRecycles=0;const o=e.pool.socks.length;return S(e,"info",`service up: ${e.project} (${o} pods, ${r})`,`cluster-service:${e.project}`,{kind:"service-up",project:e.project,pods:o,adopted:r==="adopted"}),!0},"markUp"),we=a(async e=>{if(e.manualStopped)return!1;if(O(e.pool)>0){const s=await Ie(e);if(e.manualStopped)return!1;if(s.length>0&&(console.error(`[gateway] ${e.project}: ${s.length} adopted pod(s) hold a foreign east-west key \u2014 evicting instead of adopting.`),S(e,"warning",`stale adoption: ${e.project} (foreign east-west key)`,`cluster-service:${e.project}`,{kind:"pod-recycle",project:e.project,reason:"stale-adoption",pods:s.length}),e.proc&&V(e,"stale adoption (foreign east-west key)"),Oe(e,s)),e.pool.socks.length>0)return pe(e,"adopted"),!0}if(t.stopped())return!1;let r=!1;const o=a(()=>e.state!=="starting"&&!e.proc,"notStarted");if(o())if(e.coldWait)await e.coldWait;else{E>=$&&(console.log(`[gateway] cold start queued: ${e.project}${e.wsKey&&e.wsKey!==t.wsKey?` (${e.wsKey})`:""} \u2014 ${E} starting, ${b.length} ahead (LENSMCP_MAX_COLD_STARTS=${$}).`),S(e,"info",`cold start queued: ${e.project}`,`cluster-service:${e.project}`,{kind:"cold-start-queued",project:e.project,inFlight:E,ahead:b.length}));const s=F();if(e.coldWait=s,await s,e.coldWait=void 0,r=!0,t.stopped()||e.manualStopped)return D(),!1;o()&&Y(e)}try{return await Ne(e)}finally{r&&D()}},"ensureUp"),Ne=a(async e=>{const r=Date.now()+t.startupTimeoutMs,o=Date.now()+12e3;for(;!t.stopped()&&!e.manualStopped&&Date.now()<r;){if(await sleep(Math.min(700,t.startupTimeoutMs/4)),e.manualStopped||e.buildFailed&&Date.now()>o)return!1;if(O(e.pool)>0){e.state="up",e.buildFailed=!1;const s=e.spawnAt?Date.now()-e.spawnAt:void 0;return console.log(`[gateway] ${e.project} up (${e.pool.socks.length} pods${s?`, cold start ${s}ms`:""})`),S(e,"info",`cold start: ${e.project} (${e.pool.socks.length} pods, ${s??"?"}ms)`,`cluster-service:${e.project}`,{kind:"cold-start",project:e.project,pods:e.pool.socks.length,durationMs:s}),!0}if(e.state==="down")return!1}return!1},"waitForPods"),Ke=a(e=>{const r=e.decl.maxPods??e.decl.minPods??1,o=e.pool.socks.length,s=scaleTarget(e);!s||o===0||o>=r||e.inflight<=o||Date.now()-e.lastScaleAt<5e3||(e.lastScaleAt=Date.now(),postScale(s,o+1,l.scaleLookup),console.log(`[gateway] autoscale ${e.project}: ${o} \u2192 ${o+1} pods (inflight ${e.inflight})`),S(e,"info",`autoscale: ${e.project} ${o} \u2192 ${o+1} pods`,`cluster-service:${e.project}`,{kind:"autoscale",project:e.project,from:o,to:o+1,inflight:e.inflight}))},"scaleUp"),ue=l.killGraceMs??types_1.TREE_KILL_GRACE_MS,N=new Map,Ce=a(e=>{const r=N.get(e);if(!r||(N.delete(e),clearTimeout(r.timer),!L(e)))return;const{svc:o,why:s}=r;console.log(`[gateway] ${o.project}: its tree (pid ${e.pid}) was still running ${Math.round(ue/1e3)}s after it was told to stop (${s}) \u2014 killing it (SIGKILL).`),S(o,"warning",`tree force-killed: ${o.project} (it ignored SIGTERM)`,`cluster-service:${o.project}`,{kind:"tree-force-killed",project:o.project,why:s,pid:e.pid,graceMs:ue}),T(e,"SIGKILL")},"settleDyingTree"),Se=a((e,r,o)=>{if(A(r,"SIGTERM"),ue<=0||N.has(r))return;const s=setTimeout(()=>Ce(r),ue);s.unref?.(),N.set(r,{svc:e,why:o,timer:s})},"stopTree"),Fe=a(async(e=3e3)=>{const r=Date.now()+e;for(;N.size>0;){for(const[o,s]of[...N])L(o)||(clearTimeout(s.timer),N.delete(o));if(N.size===0||Date.now()>=r)break;await sleep(100)}for(const o of[...N.keys()])Ce(o)},"finishDyingTrees"),V=a((e,r,o)=>{if(!e.proc)return;console.log(`[gateway] ${e.project} ${r} \u2014 scaling to zero.`),o?.announce!==!1&&S(e,"info",`idle kill: ${e.project} (${r})`,`cluster-service:${e.project}`,{kind:"idle-kill",project:e.project,why:r});const s=e.proc;try{s.stdout?.removeAllListeners("data"),s.stderr?.removeAllListeners("data"),s.removeAllListeners("exit")}catch{}(0,discovery_1.stopRoutingTo)(e.pool),Se(e,s,r),e.proc=void 0,e.state="down",setTimeout(()=>{ge(e),e.pool.socks=[],e.pool.scannedAt=0},1200).unref?.()},"killSpawned"),ge=a(e=>{try{for(const r of fs.readdirSync(e.pool.dir)){if(!r.endsWith(".sock"))continue;const o=path.join(e.pool.dir,r);try{const s=Number(fs.readFileSync(`${o}.owner`,"utf8").trim());if(Number.isFinite(s)&&s>0)try{process.kill(s,0);continue}catch{}}catch{}try{fs.unlinkSync(o)}catch{}try{fs.unlinkSync(`${o}.owner`)}catch{}}}catch{}},"sweepDeadOwnerSocks");let Q=0;const ve=[],We=a(()=>Q<X?(Q+=1,Promise.resolve()):new Promise(e=>{ve.push(()=>{Q+=1,e()})}),"acquireRollSlot"),Ge=a(()=>{Q-=1,ve.shift()?.()},"releaseRollSlot"),ye=a(async(e,r,o)=>{O(e.pool);for(const s of e.pool.socks){const i=await f(s,void 0,o);if(i&&r(i.podGeneration))return!0}return!1},"anyPodAnswers"),he=new Map,qe=a((e,r,o)=>{const s=C>0&&(0,handoff_retire_1.requestHandoffRetire)(e.pool.dir,o);if(A(r,"SIGTERM"),!s)return;const i=e.wsKey&&e.wsKey!==t.wsKey&&e.root?(0,workspace_log_1.workspaceLogWriter)(e.root):void 0,n=a(ne=>h=>{if(t.stopped())return;const P=(0,inspector_secret_1.redactInspectorUrls)(h.toString()).replace(/^(?=.)/gm,`[${e.project} g${o} retiring] `);ne.write(P),i?.(P)},"say"),d=n(process.stdout),u=n(process.stderr);r.stdout?.on("data",d),r.stderr?.on("data",u);let w,k=!1;const j=a(()=>{k=!0,clearTimeout(w),r.stdout?.removeListener("data",d),r.stderr?.removeListener("data",u),r.removeListener("close",j)},"releaseLogs");r.once("close",j);let v=!1;const g=a(()=>{v||(v=!0,clearTimeout(R),clearInterval(G),t.stopped()?j():k||(w=setTimeout(j,1e3),w.unref?.()),he.delete(r),(0,handoff_retire_1.clearHandoffRetire)(e.pool.dir,o),ge(e),e.pool.scannedAt=0)},"end"),R=setTimeout(()=>{console.error(`[gateway] pod handoff: ${e.project} g${o} was still running ${C}ms after it was retired \u2014 killing it.`),T(r,"SIGKILL"),g()},C);R.unref?.();const M=a(()=>{if(!v){if(t.stopped()){g();return}L(r)||(console.log(`[gateway] pod handoff: ${e.project} g${o} finished its in-flight work and exited.`),g())}},"look"),G=setInterval(M,ie);G.unref?.(),r.once("exit",M),he.set(r,()=>{g(),Se(e,r,"the gateway is stopping")})},"retireReplacedTree"),Ve=l.standbyGraceMs??3e4,I=new Map,Ue=a((e,r,o,s,i)=>{const n=i+B+Ve,d=setTimeout(()=>Me(r),Math.max(0,n-Date.now()));d.unref?.(),I.set(r,{svc:e,generation:o,reason:s,beganAt:i,deadlineAt:n,timer:d})},"holdStandby"),Be=a(e=>{const r=I.get(e);r&&(clearTimeout(r.timer),I.delete(e))},"releaseStandby"),Z=a((e,r)=>{const o=I.get(e);if(!o)return!1;clearTimeout(o.timer),I.delete(e);const{svc:s,generation:i}=o;try{e.stdout?.removeAllListeners("data"),e.stderr?.removeAllListeners("data")}catch{}return(0,discovery_1.stopRoutingTo)(s.pool,n=>(0,pod_sock_1.podSockGeneration)(n)===i),Se(s,e,`standby g${i}: ${r}`),setTimeout(()=>ge(s),1200).unref?.(),!0},"endStandby"),Te=a((e,r)=>{const o=r.oldServing?`old generation g${r.fromGen} kept serving`:`and the old generation g${r.fromGen} is NOT serving (${r.oldGone?"its tree is gone":"none of its pods answers"}) \u2014 the service is down until it recovers`;console.error(`[gateway] pod handoff failed: ${e.project} g${r.generation}: ${r.why} \u2014 ${o}.`),e.rollFailure=`${r.why} \u2014 ${o}`,S(e,r.oldServing?"warning":"error",`pod handoff failed: ${e.project}`,`cluster-service:${e.project}`,{kind:"pod-handoff-failed",project:e.project,reason:r.reason,generation:r.generation,error:r.why,durationMs:Date.now()-r.beganAt,oldServing:r.oldServing})},"reportStandbyFailed"),Me=a(e=>{const r=I.get(e);if(!r)return;const{svc:o,generation:s,reason:i,beganAt:n}=r,d=`the standby outlived its roll (${Math.round((Date.now()-n)/1e3)}s; a roll waits ${Math.round(B/1e3)}s for it)`;Z(e,d);const u=o.proc,w=t.serviceKeys[(0,service_keys_1.serviceKeyId)(o.wsKey,o.project)];(async()=>{const k=!u||u.exitCode!==null||u.signalCode!==null,j=k?!1:await Promise.race([ye(o,g=>g!==s,w).catch(()=>!1),sleep(2e3).then(()=>{})]),v=!t.stopped()&&!k&&(j??O(o.pool)>0);Te(o,{reason:i,generation:s,fromGen:o.generation??s-1,why:d,beganAt:n,oldServing:v,oldGone:k})})()},"outlivedRoll"),He=a(()=>{for(const[e,r]of[...I])Date.now()>=r.deadlineAt?Me(e):!r.svc.rolling&&r.svc.proc!==e&&(console.error(`[gateway] pod handoff: ${r.svc.project} g${r.generation} is a standby whose roll is over \u2014 stopping it.`),Z(e,"its roll is over"))},"sweepStandbys"),Ye=a(()=>{for(const e of[...he.values()])e();for(const e of[...I.keys()])Z(e,"the gateway is stopping")},"stopRetiringTrees"),me=a((e,r)=>{if(e.rolling)return e.rolling;const o=e.proc;if(!o||t.stopped())return Promise.resolve(!1);if(e.launcher==="nx")return console.log(`[gateway] pod handoff skipped: ${e.project} runs through nx run, which starts one instance of a target at a time \u2014 no standby can overlap it (reason=${r}).`),Promise.resolve(!1);Q>=X&&console.log(`[gateway] pod handoff queued: ${e.project} reason=${r} \u2014 ${Q} roll(s) in flight, ${ve.length} ahead (LENSMCP_POD_MAX_CONCURRENT_ROLLS=${X}).`);const s=(async()=>{await We();try{return await Xe(e,o,r)}finally{Ge()}})().finally(()=>{e.rolling=void 0});return e.rolling=s,s},"rollPod"),Xe=a(async(e,r,o)=>{const s=e.proc;if(s!==r||t.stopped())return!1;const i=e.generation??0,n=i+1,d=Date.now();e.lastStaleRecycleAt=d,console.log(`[gateway] pod handoff start: ${e.project} g${i}\u2192g${n} reason=${o} \u2014 standby spawning, old generation keeps serving.`),S(e,"info",`pod handoff starting: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"pod-handoff-start",project:e.project,reason:o,generation:n,fromGeneration:i});const u=J(e,n,"standby"),w=t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)];Ue(e,u,n,o,d);let k=!1;try{const j=d+B;let v;for(;!t.stopped()&&Date.now()<j&&I.has(u)&&!(u.exitCode!==null||u.signalCode!==null);){O(e.pool);for(const g of e.pool.socks)if((await f(g,void 0,w))?.podGeneration===n){v=g;break}if(v)break;await sleep(le)}if(!I.has(u))return!1;if(!v||t.stopped()){const g=t.stopped()?"gateway stopping":u.exitCode!==null||u.signalCode!==null?`standby exited (${u.exitCode??u.signalCode})`:`standby not ready within ${Math.round(B/1e3)}s`;Z(u,g);const R=!t.stopped()&&e.proc===s&&s.exitCode===null&&s.signalCode===null&&await ye(e,M=>M!==n,w);return Te(e,{reason:o,generation:n,fromGen:i,why:g,beganAt:d,oldServing:R,oldGone:e.proc!==s||s.exitCode!==null||s.signalCode!==null}),!1}if(e.proc!==s)return Z(u,"the service was replaced underneath the roll"),!1;Be(u),k=!0,e.proc=u,e.launcher=oe.get(u),e.generation=n,e.spawnAt=d,e.state="up",e.watcher=void 0,e.staleVerifiedAt=void 0,e.staleKeptAt=void 0,e.buildFailed=!1,e.lastErrors=[];try{s.stdout?.removeAllListeners("data"),s.stderr?.removeAllListeners("data"),s.removeAllListeners("exit")}catch{}return(0,discovery_1.stopRoutingTo)(e.pool,g=>(0,pod_sock_1.podSockGeneration)(g)!==n&&path.join(e.pool.dir,g)!==v),qe(e,s,i),setTimeout(()=>{ge(e),e.pool.scannedAt=0},1200).unref?.(),console.log(`[gateway] pod handoff ready: ${e.project} g${n} answered on ${path.basename(v)} in ${Date.now()-d}ms; retiring g${i}.`),S(e,"info",`pod handoff complete: ${e.project}`,`cluster-service:${e.project}`,{kind:"pod-handoff-complete",project:e.project,reason:o,generation:n,fromGeneration:i,durationMs:Date.now()-d}),!0}finally{k||Z(u,"its roll ended without it")}},"runRoll"),ze=a((e,r)=>{if(t.stopped())return{refused:"the gateway is stopping"};if(e.manualStopped=!1,e.actionFailure=void 0,e.restart&&!e.restart.done)return{restart:e.restart,already:!0};if(!e.proc&&e.state!=="down")return{refused:`${e.project} is served by pods this gateway adopted from an earlier one \u2014 it has no tree of its own to replace. \`lensmcp gateway restart\` starts every service under this gateway.`};const o=e.proc?r.hard||e.launcher==="nx"||e.state!=="up"?"hard":"rolling":"start",s={how:o,reason:r.reason,startedAt:Date.now(),...e.proc&&e.generation!==void 0?{fromGeneration:e.generation}:{}};return e.restart=s,e.lastUsed=s.startedAt,console.log(`[gateway] restart asked: ${e.project}${e.wsKey!==t.wsKey?` (${e.wsKey})`:""} \u2014 ${o} (${r.reason}).`),S(e,"info",`restart asked: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"service-restart",phase:"started",project:e.project,how:o,reason:r.reason,...s.fromGeneration!==void 0?{fromGeneration:s.fromGeneration}:{}}),a(async()=>{if(o==="start")return await we(e)?{ok:!0}:{ok:!1,error:$e(e)};if(o==="rolling")return e.rolling&&await e.rolling,e.proc?(e.rollFailure=void 0,await me(e,r.reason)?{ok:!0}:{ok:!1,error:e.rollFailure??"the new tree did not take over"}):await we(e)?{ok:!0}:{ok:!1,error:$e(e)};if(V(e,`restart asked (${r.reason})`,{announce:!1}),await sleep(1500),t.stopped())return{ok:!1,error:"the gateway is stopping"};let i=!1;e.proc||(await F(),i=!0,!e.proc&&e.state!=="starting"&&Y(e));try{const n=e.proc,d=e.generation,u=t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)],w=Date.now()+t.startupTimeoutMs;for(;!t.stopped()&&Date.now()<w;){if(!n||e.proc!==n||n.exitCode!==null||n.signalCode!==null)return{ok:!1,error:$e(e)};if(await ye(e,k=>k===d,u))return pe(e,"observed"),e.buildFailed=!1,{ok:!0};await sleep(le)}return{ok:!1,error:t.stopped()?"the gateway is stopping":`no pod of the new tree answered within ${Math.round(t.startupTimeoutMs/1e3)}s`}}finally{i&&D()}},"run")().catch(i=>({ok:!1,error:i.message})).then(i=>{s.done={ok:i.ok,at:Date.now(),...e.generation!==void 0?{generation:e.generation}:{},...i.error?{error:i.error}:{}},i.ok&&(e.lastUsed=s.done.at);const n=Math.round((s.done.at-s.startedAt)/100)/10;i.ok?console.log(`[gateway] restarted: ${e.project} \u2014 g${s.done.generation??"?"} serving after ${n}s (${o}).`):console.error(`[gateway] restart failed: ${e.project} \u2014 ${i.error} (${o}, after ${n}s).`),S(e,i.ok?"info":"error",i.ok?`restarted: ${e.project}`:`restart failed: ${e.project}`,`cluster-service:${e.project}`,{kind:"service-restart",phase:i.ok?"done":"failed",project:e.project,how:o,reason:r.reason,...s.fromGeneration!==void 0?{fromGeneration:s.fromGeneration}:{},...s.done.generation!==void 0?{generation:s.done.generation}:{},durationMs:s.done.at-s.startedAt,...i.error?{error:i.error}:{}})}),{restart:s,already:!1}},"restartService"),$e=a(e=>e.buildFailed?`its build failed${e.lastErrors?.length?`: ${e.lastErrors[e.lastErrors.length-1]}`:""}`:e.proc&&(e.proc.exitCode!==null||e.proc.signalCode!==null)?`its tree exited (${e.proc.exitCode??e.proc.signalCode})`:!e.proc&&e.state==="down"?"its tree exited before a pod answered":`no pod answered within ${Math.round(t.startupTimeoutMs/1e3)}s`,"startFailure"),xe=a(e=>{const r=Date.now();e.restarts=(e.restarts??[]).filter(s=>r-s<types_1.SERVICE_RESTART_WINDOW_MS);const o=(e.wedgeRecycles??0)+1;if(e.restarts.length>=types_1.SERVICE_MAX_RESTARTS||o>types_1.SERVICE_MAX_RESTARTS){const s=e.restarts.length>=types_1.SERVICE_MAX_RESTARTS?`wedged ${types_1.SERVICE_MAX_RESTARTS}+\xD7/min`:`recycled ${types_1.SERVICE_MAX_RESTARTS}\xD7 in a row and no pod came up`;S(e,"error",`service crash-looping: ${e.project} \u2014 ${s}, not auto-recycling (the next request retries)`,`cluster-service:${e.project}`,{kind:"service-crash-loop",project:e.project,recycles:o-1}),V(e,"wedged (crash-loop cap) \u2014 leaving down for the next request",{announce:!1});return}e.wedgeRecycles=o,e.restarts.push(r),console.log(`[gateway] self-heal: recycling WEDGED ${e.project} (live parent, 0 live pods; attempt ${o}/${types_1.SERVICE_MAX_RESTARTS} with no pod up since the first, ${e.restarts.length} in the last minute; ${e.inflight??0} request(s) in flight \u2014 jobs held by the dead pod keep their locks until they expire)`),S(e,"warning",`self-heal: recycling wedged ${e.project} (0 live pods)`,`cluster-service:${e.project}`,{kind:"service-recycle",project:e.project,attempt:o,inWindow:e.restarts.length}),V(e,"wedged: 0 live pods \u2014 recycling",{announce:!1}),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&Y(e)},1500).unref?.()},"recycleWedged"),Je={maxAgeMs:types_1.POD_MAX_AGE_MS,graceMs:types_1.POD_STALE_GRACE_MS,settleMs:types_1.POD_RECYCLE_SETTLE_MS,cooldownMs:types_1.POD_STALE_RECYCLE_COOLDOWN_MS};let _e;const se=new Map,Ee=new Map;let ke=!1,je=0,fe=0;const Qe=a(()=>{const e=t.projectRoots??{},r=new Set(t.services.map(s=>s.project));for(const s of t.routes)s.lens&&r.add(s.lens.project);const o=new Set;for(const s of r){const i=e[s];if(!i)continue;const n=path.relative(t.root,i).split(path.sep).filter(Boolean)[0];n&&!n.startsWith("..")&&o.add(n)}return[...o]},"managedBuckets"),Ze=a((e,r,o,s)=>{const i=t.projectSourceRoots?.[e];if(i?.length)return i;const n=t.projectRoots?.[e];if(!n)return;const d=sourceScopeDirs(t.root,n,o,r,s);return d.length>0?d:void 0},"scopeFor"),et=a(()=>{if(ke||Date.now()-je<types_1.POD_STALE_SCAN_MS)return;ke=!0,je=Date.now();const e=je;(async()=>{const r=(0,scope_1.readSourceSetOptions)(t.root),o=r.exclude,s={ignoreTests:r.ignoreTests},i=sourceScanPass(t.root,o,s),n=await i.snapshot(),d=Date.now(),u=sourceChangeWindowMs(d,fe,types_1.POD_STALE_SCAN_MS,types_1.POD_RECYCLE_SETTLE_MS),w=_e?diffSourceSets(_e,n,d,u):null;if(w&&(t.sourceSetChangedAt=d,t.sourceSetChange=w,H("info","source file set changed \u2014 stale pods recycle when idle","cluster-gateway",{kind:"source-set-changed",added:w.added,removed:w.removed,truncated:w.truncated})),_e=n,!t.projectRoots){fe=e;return}let k;try{k=(await fs.promises.readdir(t.root,{withFileTypes:!0})).filter(h=>h.isDirectory()).map(h=>h.name)}catch{return}const j=Qe(),v=new Set(t.services.map(h=>h.project));for(const h of t.routes)h.lens&&v.add(h.lens.project);const g=new Map;for(const h of v){const P=Ze(h,k,j,o);if(!P)continue;const x=P.join("|"),q=g.get(x)??{dirs:P,projects:[]};q.projects.push(h),g.set(x,q)}const R=Date.now(),M=sourceChangeWindowMs(R,fe,types_1.POD_STALE_SCAN_MS,types_1.POD_RECYCLE_SETTLE_MS);fe=e;const G=a((h,P,x)=>{t.sourceSetChangedFor??=new Map,t.sourceSetChangesFor??=new Map;for(const q of h)t.sourceSetChangedFor.set(q,P),t.sourceSetChangesFor.set(q,x)},"stampScoped"),ne=new Set([...g.values()].flatMap(h=>h.projects));for(const h of[...t.sourceSetScoped??[]])ne.has(h)||t.sourceSetScoped.delete(h);for(const[h,{dirs:P,projects:x}]of g){const q=await i.snapshot(P),Re=se.get(h);if(Re||x.some(K=>Ee.has(K))){t.sourceSetScoped??=new Set;for(const K of x)t.sourceSetScoped.add(K)}se.set(h,q);const Ae=new Map;for(const K of x){const U=Ee.get(K);if(U===void 0||U===h)continue;const ee=Ae.get(U)??[];ee.push(K),Ae.set(U,ee)}for(const K of x)Ee.set(K,h);for(const[K,U]of Ae){const ee=se.get(K),ce=ee?diffSourceSets(ee,q,R,M):w;ce&&(G(U,ee?R:d,ce),H("info",`source file set changed \u2014 ${U.join(", ")}'s watch scope was re-keyed`,"cluster-gateway",{kind:"source-set-changed",scoped:!0,rescoped:!0,projects:U,added:ce.added,removed:ce.removed,truncated:ce.truncated}))}if(!Re)continue;const ae=diffSourceSets(Re,q,R,M);ae&&(G(x,R,ae),H("info",`source file set changed in ${x.join(", ")}'s scope`,"cluster-gateway",{kind:"source-set-changed",scoped:!0,projects:x,added:ae.added,removed:ae.removed,truncated:ae.truncated}))}for(const h of[...se.keys()])g.has(h)||se.delete(h)})().catch(()=>{}).finally(()=>{ke=!1})},"maybeScanSourceSet"),tt=a((e,r)=>{const o=podChangeClock(t,e.project),s=o.change,i=`${o.scope==="project"?"in its scope":"in the workspace (its scope is not known)"}: ${changeEvidence(s)}`,n=s?.windowMs??types_1.POD_STALE_SCAN_MS+types_1.POD_RECYCLE_SETTLE_MS,d=podWatcherVerdict(e.watcher,r,n),u=a(k=>(e.staleKeptAt!==r&&(e.staleKeptAt=r,console.log(`[gateway] stale source set: pod ${e.project} ${k} \u2014 no recycle.`)),!1),"keep");if(d==="heard")return e.staleVerifiedAt=r,S(e,"info",`stale check: ${e.project} watcher confirmed \u2014 no recycle`,`cluster-service:${e.project}`,{kind:"pod-stale-verified",project:e.project,watcher:d}),u(`watcher CONFIRMED alive across the change (last build ${new Date(e.watcher?.lastBuildAt??0).toISOString()})`);if(d==="degraded")return console.log(`[gateway] stale source set: pod ${e.project} watcher DEGRADED (${e.watcher?.degraded}) \u2014 recycling.`),!0;const w=a(k=>(e.staleKeptAt!==r&&(e.staleKeptAt=r,console.log(`[gateway] stale source set: pod ${e.project} ${k}; the change ${i} \u2014 recycling.`)),!0),"recycling");return d==="silent"?types_1.POD_WATCHER_SILENT==="keep"?u("watcher alive but quiet across the change \u2014 keeping it (LENSMCP_POD_WATCHER_SILENT=keep; a live watcher cannot miss an add it will need)"):w("watcher alive but quiet across the change"):types_1.POD_WATCHER_UNVERIFIED==="keep"?u("unverifiable (no watcher testimony \u2014 older devserver bundle?) \u2014 keeping it (LENSMCP_POD_WATCHER_UNVERIFIED=keep)"):w("watcher unverifiable (no testimony yet \u2014 a pod that never rebuilt, or an older devserver bundle)")},"podStaleRecycleAllowed"),rt=a((e,r)=>{if(e.lastStaleRecycleAt=Date.now(),types_1.POD_RECYCLE_MODE==="rolling"&&e.proc&&e.launcher!=="nx"){me(e,r);return}console.log(`[gateway] zombie recycle: ${e.project} (${r}) \u2014 stale fs view; fresh pod incoming`),S(e,"warning",`zombie recycle: ${e.project} (${r})`,`cluster-service:${e.project}`,{kind:"pod-recycle",project:e.project,reason:r});const o=!!e.decl.eager;V(e,`${r} \u2014 recycling stale pod`,{announce:!1}),o&&setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&Y(e)},1500).unref?.()},"recyclePod"),ot=a(()=>{const e=setInterval(()=>{ct().catch(r=>console.error("[gateway] sweep error:",r))},t.sweepMs);return e.unref(),e},"startSweeper"),st=l.listProcesses??(()=>{try{return(0,orphan_reaper_1.parsePsRows)((0,node_child_process_1.execFileSync)("ps",orphan_reaper_1.PS_ROW_ARGS,{encoding:"utf8",timeout:5e3}))}catch{return[]}}),Pe=l.signalProcess??((e,r)=>{process.kill(e,r)});let De=0;const nt=a(()=>{if(typeof process.getuid!="function")return 0;const e=t.registry?t.registry.allServices():t.services;if(e.length===0)return 0;const r=e.map(n=>({label:n.wsKey||path.basename(n.root),project:n.project,root:n.root,svc:n})),o=new Set;for(const n of e)n.proc?.pid&&o.add(n.proc.pid);for(const n of[...he.keys(),...N.keys(),...I.keys()])n.pid&&o.add(n.pid);const s=[...new Set((0,process_name_1.defaultLinkDirs)().flatMap(n=>{try{return[n,fs.realpathSync(n)]}catch{return[n]}}))];let i=0;for(const n of(0,orphan_reaper_1.findOrphanExecutors)(st(),r,{linkDirs:s,uid:process.getuid(),selfPid:process.pid,ownGroups:o})){const{row:d}=n;try{Pe(n.group?-d.pgid:d.pid,"SIGKILL")}catch(w){if(w.code!=="ESRCH"||!n.group)continue;try{Pe(d.pid,"SIGKILL")}catch{continue}}i+=1;const u=n.owner.svc;console.log(`[gateway] reaped an orphaned executor of ${u.project} (${n.role}, pid ${d.pid}${n.group?`, group ${d.pgid}`:""}, parent gone) \u2014 a stopped tree it outlived: ${d.command.slice(0,160)}`),S(u,"warning",`orphan reap: ${u.project} (pid ${d.pid})`,`cluster-service:${u.project}`,{kind:"orphan-executor-reap",project:u.project,pid:d.pid,pgid:d.pgid,role:n.role,group:n.group})}return i},"reapOrphanExecutors"),ct=a(async()=>{{if(t.stopped())return;He(),l.reapForeign!==!1&&types_1.ORPHAN_REAP_EVERY_MS>0&&Date.now()-De>=types_1.ORPHAN_REAP_EVERY_MS&&(De=Date.now(),nt());for(const e of t.graphStores?.all()??[])e.refreshIfStale();types_1.POD_RECYCLE_ENABLED&&et();for(const e of t.registry?t.registry.allServices():t.services){if(t.stopped()||!e.proc){e.wedgedSince=void 0;continue}const r=path.resolve(e.root)===path.resolve(t.root),o=(0,warm_start_1.instanceIdleSec)(e,!r&&!!t.registry?.get(e.wsKey)?.instance);if(o&&e.state==="up"&&(!e.restart||e.restart.done)&&e.inflight===0&&Date.now()-e.lastUsed>o*1e3){V(e,`idle ${o}s`),e.wedgedSince=void 0;continue}const s=e.state==="starting"&&e.spawnAt!==void 0&&Date.now()-e.spawnAt<t.startupTimeoutMs,i=O(e.pool)>0;if(i||s){if(e.wedgedSince=void 0,i&&pe(e,"observed"),i&&!s&&types_1.POD_RECYCLE_ENABLED&&r){const n=podChangeClock(t,e.project).at;await be(e);const d=(e.staleVerifiedAt??0)>=n&&n>0,u=podRecycleReason(e,Date.now(),d?0:n,Je);if(u==="stale-source-set"&&!tt(e,n))continue;u&&rt(e,u)}continue}e.wedgedSince??=Date.now(),Date.now()-e.wedgedSince>types_1.WEDGE_RECYCLE_GRACE_MS&&(e.wedgedSince=void 0,xe(e))}}},"sweepOnce");return{scanNow:O,spawnService:Y,ensureUp:we,scaleUp:Ke,killSpawned:V,markUp:pe,startSweeper:ot,rollPod:me,restartService:ze,recycleWedged:xe,stopRetiringTrees:Ye,finishDyingTrees:Fe,stopBuildHost:a(e=>e===void 0?_.stop():_.release(e),"stopBuildHost")}}m(createServiceLayer,"createServiceLayer"),a(createServiceLayer,"createServiceLayer");function chooseStalePodKillTarget(t,c,l){if(!(t===void 0||!Number.isInteger(t)||t<=1)&&!(c!==void 0&&c!==t)&&t!==l)return t}m(chooseStalePodKillTarget,"chooseStalePodKillTarget"),a(chooseStalePodKillTarget,"chooseStalePodKillTarget");
|
|
1
|
+
"use strict";var it=Object.defineProperty;var m=(t,c)=>it(t,"name",{value:c,configurable:!0});var at=Object.defineProperty,a=m((t,c)=>at(t,"name",{value:c,configurable:!0}),"a");Object.defineProperty(exports,"__esModule",{value:!0}),exports.scaleTarget=scaleTarget,exports.isLoopbackHostname=isLoopbackHostname,exports.isOwnRunnerCommand=isOwnRunnerCommand,exports.isRunnerOf=isRunnerOf,exports.isTestSourcePath=isTestSourcePath,exports.sourceScanPass=sourceScanPass,exports.sourceSetSnapshot=sourceSetSnapshot,exports.sourceSetSignature=sourceSetSignature,exports.diffSourceSets=diffSourceSets,exports.sourceChangeWindowMs=sourceChangeWindowMs,exports.sourceScopeDirs=sourceScopeDirs,exports.podRecycleReason=podRecycleReason,exports.lensFrontendStale=lensFrontendStale,exports.lensWatcherVerdict=lensWatcherVerdict,exports.podWatcherVerdict=podWatcherVerdict,exports.lensChildWedged=lensChildWedged,exports.isPortConflictExit=isPortConflictExit,exports.podChangeClock=podChangeClock,exports.createServiceLayer=createServiceLayer,exports.chooseStalePodKillTarget=chooseStalePodKillTarget;const tslib_1=require("tslib"),node_child_process_1=require("node:child_process"),node_crypto_1=require("node:crypto"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),net=tslib_1.__importStar(require("node:net")),path=tslib_1.__importStar(require("node:path")),node_util_1=require("node:util"),discovery_1=require("./discovery"),pod_sock_1=require("../../../pod-sock"),scope_1=require("./scope"),pod_env_1=require("./pod-env"),pod_probe_1=require("./pod-probe"),service_keys_1=require("./service-keys"),workspace_log_1=require("./workspace-log"),own_log_1=require("./own-log"),inspector_secret_1=require("../../../inspector-secret"),pod_output_1=require("./pod-output"),serve_launch_1=require("./serve-launch"),shared_build_1=require("./shared-build"),awake_1=require("./awake"),warm_start_1=require("./warm-start"),protocol_1=require("../../../shared-build/protocol"),types_1=require("./types"),exit_reason_1=require("../../../exit-reason"),handoff_retire_1=require("../../../handoff-retire"),process_name_1=require("../../../process-name"),orphan_reaper_1=require("./orphan-reaper");function eastWestEnv(t,c){const i={},p=t.serviceKeys[(0,service_keys_1.serviceKeyId)(c.wsKey,c.project)];if(p&&(i.LENSMCP_SERVICE_KEY=p),t.https){const w=require("../../../basic-ssl").caCertPath();fs.existsSync(w)&&(i.NODE_EXTRA_CA_CERTS=w)}return i}m(eastWestEnv,"eastWestEnv"),a(eastWestEnv,"eastWestEnv");const sleep=a(t=>new Promise(c=>setTimeout(c,t)),"sleep"),SRC_EXCLUDE=new Set(["node_modules","dist","tmp","coverage","build","out","target"]),SRC_EXT=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".css",".scss",".sass",".less",".json",".html",".htm",".vue",".svelte",".mdx"]),SCAN_FILE_CAP=1e5;function fnv1a(t){let c=2166136261;for(let i=0;i<t.length;i+=1)c^=t.charCodeAt(i),c=Math.imul(c,16777619);return c>>>0}m(fnv1a,"fnv1a"),a(fnv1a,"fnv1a");const execFileP=(0,node_util_1.promisify)(node_child_process_1.execFile);async function gitIgnoredPaths(t){try{const{stdout:c}=await execFileP("git",["ls-files","--others","--directory","--ignored","--exclude-standard","-z"],{cwd:t,maxBuffer:33554432,timeout:1e4,windowsHide:!0});return new Set(c.split("\0").filter(Boolean))}catch{return new Set}}m(gitIgnoredPaths,"gitIgnoredPaths"),a(gitIgnoredPaths,"gitIgnoredPaths");function scaleTarget(t,c=fs.existsSync){if(t.decl.adminUrl)return{url:t.decl.adminUrl};const i=path.join(t.pool.dir,"parent.sock");return c(i)?{socketPath:i}:t.decl.port?{url:`http://localhost:${t.decl.port}`}:void 0}m(scaleTarget,"scaleTarget"),a(scaleTarget,"scaleTarget");function isLoopbackHostname(t){const c=t.toLowerCase().replace(/^\[(.*)\]$/,"$1").replace(/\.$/,"");return c==="localhost"||c.endsWith(".localhost")?!0:net.isIPv4(c)?c.split(".")[0]==="127":net.isIPv6(c)&&new URL(`http://[${c}]/`).hostname==="[::1]"}m(isLoopbackHostname,"isLoopbackHostname"),a(isLoopbackHostname,"isLoopbackHostname");function postScale(t,c,i){let p;if("socketPath"in t)p=http.request({socketPath:t.socketPath,path:"/webpack/scale",method:"POST",headers:{"content-type":"application/json"}},w=>w.resume());else{const w=t.url.startsWith("https"),T=w?require("node:https"):http,_={method:"POST"};w&&isLoopbackHostname(new URL(t.url).hostname)&&(_.rejectUnauthorized=!1),i&&(_.lookup=i),p=T.request(`${t.url}/webpack/scale`,_,A=>A.resume())}return p.on("error",()=>{}),p.end(JSON.stringify({pods:c})),p}m(postScale,"postScale"),a(postScale,"postScale");function isOwnRunnerCommand(t,c){return(0,serve_launch_1.isServeRunnerCommand)(t)?(0,serve_launch_1.isServeRunnerCommandOf)(t,c):t.includes("/node_modules/nx/")?t.includes(`${c.replace(/\/+$/,"")}/node_modules/nx/`):!0}m(isOwnRunnerCommand,"isOwnRunnerCommand"),a(isOwnRunnerCommand,"isOwnRunnerCommand");function isRunnerOf(t,c,i){const p=c.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return!new RegExp(`(^|\\s)run ${p}(:|\\s|$)`).test(t)||!(0,serve_launch_1.isServeRunnerCommand)(t)&&!/\bnx(\.js)?\b/.test(t)?!1:isOwnRunnerCommand(t,i)}m(isRunnerOf,"isRunnerOf"),a(isRunnerOf,"isRunnerOf");const TEST_FILE_RE=/\.(spec|test|e2e|e2e-spec|stories)\.[cm]?[jt]sx?$/,TEST_DIR_SEGMENTS=new Set(["__tests__","__mocks__","__snapshots__"]);function isTestSourcePath(t){const c=t.split("/"),i=c[c.length-1]??"";if(TEST_FILE_RE.test(i))return!0;for(let p=0;p<c.length-1;p+=1)if(TEST_DIR_SEGMENTS.has(c[p]))return!0;return!1}m(isTestSourcePath,"isTestSourcePath"),a(isTestSourcePath,"isTestSourcePath");function sourceScanPass(t,c,i){const p=i?.ignoreTests??!0;let w;const T=new Map,_=`tree:${path.resolve(t)}`,A=a(y=>path.relative(t,y).split(path.sep).join("/"),"rel"),C=a(async(y,$)=>{const E=await(w??=gitIgnoredPaths(t)),b=[],F=new Set;let D=!1;const H=a(async(te,S)=>{if(b.length>=SCAN_FILE_CAP){D=!0;return}let O;try{O=await fs.promises.readdir(te,{withFileTypes:!0})}catch{return}S&&F.add(A(te));for(const z of O){if(b.length>=SCAN_FILE_CAP){D=!0;return}const W=z.name,re=path.join(te,W);if(z.isDirectory()){if(!S||W.startsWith(".")||SRC_EXCLUDE.has(W)||c?.has(W)||W.endsWith("-devserver")||E.has(`${A(re)}/`))continue;await H(re,!0)}else if(z.isFile()){const oe=W.lastIndexOf(".");if(oe<=0||!SRC_EXT.has(W.slice(oe)))continue;const J=A(re);if(E.has(J)||p&&isTestSourcePath(J))continue;b.push(J)}}},"walk");return await H(y,$),{paths:b,entered:F,truncated:D}},"walkDir"),L=a((y,$)=>{let E=T.get(y);return E||(E=$(),T.set(y,E)),E},"memo"),ie=a(async()=>{const y=T.get(_);if(!y)return;const $=await y;return $.truncated?void 0:$},"wideWalk"),B=new Set,le=a(()=>L(`files:${path.resolve(t)}`,async()=>{const y=await ie();return y?{paths:y.paths.filter($=>!$.includes("/")),entered:B,truncated:!1}:C(t,!1)}),"rootFiles"),de=a(y=>L(`tree:${path.resolve(y)}`,async()=>{const $=await ie(),E=A(y);if($?.entered.has(E)){const b=`${E}/`;return{paths:E===""?$.paths:$.paths.filter(F=>F.startsWith(b)),entered:B,truncated:!1}}return C(y,!0)}),"subtree"),X=a(y=>{const $=new Set;let E=0,b=y.some(D=>D.truncated);const F=a(D=>$.has(D)?!0:$.size>=SCAN_FILE_CAP?(b=!0,!1):($.add(D),E^=fnv1a(D),!0),"add");for(const D of y)if(!D.paths.every(F))break;return{signature:`${$.size}:${(E>>>0).toString(36)}`,paths:$,truncated:b}},"compose");return{async snapshot(y){if(!y||y.length===0)return X([await L(_,()=>C(t,!0))]);const $=[await le()];for(const E of y)$.push(await de(E));return X($)}}}m(sourceScanPass,"sourceScanPass"),a(sourceScanPass,"sourceScanPass");async function sourceSetSnapshot(t,c,i,p){return sourceScanPass(t,i,p).snapshot(c)}m(sourceSetSnapshot,"sourceSetSnapshot"),a(sourceSetSnapshot,"sourceSetSnapshot");async function sourceSetSignature(t,c,i,p){return(await sourceSetSnapshot(t,c,i,p)).signature}m(sourceSetSignature,"sourceSetSignature"),a(sourceSetSignature,"sourceSetSignature");const SOURCE_DIFF_PATH_CAP=128;function diffSourceSets(t,c,i,p){if(t.paths.size===c.paths.size){let A=!0;for(const C of t.paths)if(!c.paths.has(C)){A=!1;break}if(A)return null}const w=[],T=[];let _=t.truncated||c.truncated;for(const A of c.paths)t.paths.has(A)||(w.length+T.length<SOURCE_DIFF_PATH_CAP?w.push(A):_=!0);for(const A of t.paths)c.paths.has(A)||(w.length+T.length<SOURCE_DIFF_PATH_CAP?T.push(A):_=!0);return{changedAt:i,added:w,removed:T,truncated:_,...p!==void 0?{windowMs:p}:{}}}m(diffSourceSets,"diffSourceSets"),a(diffSourceSets,"diffSourceSets");function sourceChangeWindowMs(t,c,i,p){const w=c&&c>0?t-c:i;return Math.max(w,i)+p}m(sourceChangeWindowMs,"sourceChangeWindowMs"),a(sourceChangeWindowMs,"sourceChangeWindowMs");function sourceScopeDirs(t,c,i,p,w){const T=a(C=>{const L=path.relative(t,C).split(path.sep).filter(Boolean);return L.length===0||L[0].startsWith("..")?"":L[0]},"bucketOf")(c);if(!T)return[];const _=new Set(i.filter(Boolean)),A=new Set([T]);for(const C of p)C.startsWith(".")||SRC_EXCLUDE.has(C)||w?.has(C)||C.endsWith("-devserver")||_.has(C)||A.add(C);return[...A].sort().map(C=>path.join(t,C))}m(sourceScopeDirs,"sourceScopeDirs"),a(sourceScopeDirs,"sourceScopeDirs");function podRecycleReason(t,c,i,p){return!t.proc||t.state!=="up"||t.spawnAt==null||t.inflight>0||(t.activeWork??0)>0||c-t.lastUsed<p.settleMs||t.lastStaleRecycleAt!=null&&c-t.lastStaleRecycleAt<p.cooldownMs?null:i>0&&i-t.spawnAt>p.graceMs?"stale-source-set":p.maxAgeMs>0&&c-t.spawnAt>p.maxAgeMs?"max-age":null}m(podRecycleReason,"podRecycleReason"),a(podRecycleReason,"podRecycleReason");function lensFrontendStale(t,c,i,p,w){return!(p-c<w.cooldownMs||i<=0||i-t<=w.graceMs||p-i<w.settleMs)}m(lensFrontendStale,"lensFrontendStale"),a(lensFrontendStale,"lensFrontendStale");function lensWatcherVerdict(t,c,i,p=[]){if(!t)return"unknown";if("schemaVersion"in t&&t.schemaVersion>=2&&p.length>0){const w=c-i,T=new Set(t.recentEvents.filter(_=>_.at>=w).map(_=>path.resolve(_.path)));return p.every(_=>T.has(path.resolve(_)))?"heard":"missed"}return t.lastFsSetEventAt>=c-i?"heard":"missed"}m(lensWatcherVerdict,"lensWatcherVerdict"),a(lensWatcherVerdict,"lensWatcherVerdict");function podWatcherVerdict(t,c,i){if(!t)return"unknown";if(t.degraded)return"degraded";const p=c-i;return t.lastWatchRunAt>=p||t.lastBuildAt>=p?"heard":"silent"}m(podWatcherVerdict,"podWatcherVerdict"),a(podWatcherVerdict,"podWatcherVerdict");function lensChildWedged(t,c,i,p){const w=t.hasListened?p.graceMs:Math.max(p.graceMs,p.neverListenedGraceMs);return i-t.spawnedAt<w||i-c<p.cooldownMs||t.probeFails<Math.max(2,p.failureThreshold)?null:"wedged-no-listener"}m(lensChildWedged,"lensChildWedged"),a(lensChildWedged,"lensChildWedged");function isPortConflictExit(t,c){return t.signal||t.code===null||t.code===0||t.ranMs>c.fastFailMs?!1:t.portHeld}m(isPortConflictExit,"isPortConflictExit"),a(isPortConflictExit,"isPortConflictExit");function podChangeClock(t,c){const i=t.sourceSetChangedFor?.get(c);if(i!==void 0){const p=t.sourceSetChangesFor?.get(c);return{at:i,...p?{change:p}:{},scope:"project"}}return t.sourceSetScoped?.has(c)?{at:0,scope:"none"}:{at:t.sourceSetChangedAt??0,...t.sourceSetChange?{change:t.sourceSetChange}:{},scope:"workspace"}}m(podChangeClock,"podChangeClock"),a(podChangeClock,"podChangeClock");const changeEvidence=a(t=>{if(!t)return"no path evidence";const c=[...t.added.map(i=>`+${i}`),...t.removed.map(i=>`-${i}`)];return c.length?`${c.slice(0,3).join(", ")}${c.length>3?` (+${c.length-3} more)`:""}`:"no path evidence"},"changeEvidence");function createServiceLayer(t,c,i={}){const p=i.spawnProcess??node_child_process_1.spawn,w=i.probeAlive??pod_probe_1.probePodAlive,T=i.killTree??((e,r="SIGTERM")=>{try{process.kill(-e.pid,r)}catch{try{e.kill(r)}catch{}}}),_=i.sharedBuild??(0,shared_build_1.createSharedBuildRelay)(t),A=a((e,r="SIGTERM")=>{_.detach(e),T(e,r)},"killTree"),C=i.handoffDrainMs??(0,handoff_retire_1.handoffDrainBoundMs)(),L=i.treeAlive??(e=>{if(!e.pid||!(e instanceof node_child_process_1.ChildProcess))return!1;try{return process.kill(-e.pid,0),!0}catch(r){return r.code==="EPERM"}}),ie=i.retirePollMs??1e3,B=i.rollReadyMs??types_1.POD_ROLL_READY_MS,le=i.rollPollMs??750,de=i.maxConcurrentRolls??types_1.POD_MAX_CONCURRENT_ROLLS,X=de>0?de:Number.POSITIVE_INFINITY,y=i.maxConcurrentColdStarts??types_1.MAX_COLD_STARTS,$=y>0?y:Number.POSITIVE_INFINITY;let E=0;const b=[],F=a(()=>E<$?(E+=1,Promise.resolve()):new Promise(e=>{b.push(()=>{E+=1,e()})}),"acquireColdSlot"),D=a(()=>{E-=1,b.shift()?.()},"releaseColdSlot"),{emit:H}=c,te=a(e=>e.wsKey===t.wsKey?void 0:path.join(e.root,".lensmcp","events.jsonl"),"serviceBus"),S=a((e,r,o,s,l)=>H(r,o,s,{wsKey:e.wsKey,...l},void 0,te(e)),"emitSvc"),O=a(e=>(e.scannedAt=0,(0,discovery_1.pickSock)(e,t.scanTtlMs)?e.socks.length:0),"scanNow"),z=a((e,r)=>{let o;try{o=(0,node_child_process_1.execFileSync)("ps",["ax","-ww","-o","pid=,pgid=,command="],{encoding:"utf8",timeout:5e3})}catch{return 0}const s=e.proc?.pid;let l=0;for(const n of o.split(`
|
|
2
|
+
`)){const d=n.match(/^\s*(\d+)\s+(\d+)\s+(.*)$/);if(!d)continue;const u=Number(d[1]),g=Number(d[2]);if(isRunnerOf(d[3],r,e.root)&&!(u===process.pid||u===s)&&!(s!==void 0&&g===s)){try{process.kill(-g,"SIGKILL")}catch{try{process.kill(u,"SIGKILL")}catch{}}l+=1}}return l>0&&(console.log(`[gateway] ${r}: reaped ${l} stale runner tree(s) \u2014 a foreign generation of this service (holding nx's task lock, or its pool)`),S(e,"warning",`stale-runner reap: ${e.project} (${l} foreign tree(s))`,`cluster-service:${e.project}`,{kind:"stale-runner-reap",project:e.project,reaped:l})),l},"reapForeignRunners"),W=a((e,r)=>({...(0,pod_env_1.podBaseEnv)(process.env,t.root,e.root),...e.instanceEnv?{...e.instanceEnv,LENSMCP_SOCKET_ONLY:"1"}:{},...e.frozen?{LENSMCP_FREEZE:"1",LENSMCP_TYPECHECK:"off"}:{},NX_DAEMON:"false",NX_ISOLATE_PLUGINS:"false",...e.wsKey?{LENSMCP_WS_KEY:e.wsKey}:{},...e.sockNs?{LENSMCP_SOCK_NS:e.sockNs}:{},LENSMCP_EVENT_FILE:e.wsKey===t.wsKey?t.eventFile:path.join(e.root,".lensmcp","events.jsonl"),LENSMCP_GATEWAY_PID:String(process.pid),LENSMCP_POD_GENERATION:String(r),...eastWestEnv(t,e)}),"serviceEnv"),re=a((e,r,o,s)=>{const l=W(e,r),n=e.wsKey||path.basename(e.root),d=(0,serve_launch_1.planServeLaunch)({stores:t.graphStores,root:e.root,project:e.project,target:e.decl.serveTarget??"serve-hmr",childCount:s,baseEnv:l});if(d.kind==="runner"){const v=_.offeredTo(e),f=p((0,process_name_1.namedExecPath)((0,process_name_1.processName)(n,e.project,serve_launch_1.SERVE_RUNNER_ROLE)),[...d.script.execArgv,d.script.path,...(0,serve_launch_1.serveRunnerArgs)(o,s,e.root)],{cwd:e.root,detached:!0,env:v?{...d.env,[protocol_1.SHARED_BUILD_ENV]:"1"}:d.env,stdio:["ignore","pipe","pipe","ipc"]});return v&&_.attach(e,r,f),f.send?.(d.message,R=>{R&&console.error(`[gateway] ${o}: could not hand the task to its runner (${R.message})`)}),{proc:f,launcher:"runner"}}console.log(`[gateway] ${o}: starting through nx run \u2014 ${d.reason}`);const u=path.join(e.root,"node_modules","nx","dist","bin","nx.js"),g=(0,process_name_1.namedExecPath)((0,process_name_1.processName)(n,e.project,"nx")),[k,j]=fs.existsSync(u)?[g,[u,"run",o,`--childCount=${s}`]]:["yarn",["nx","run",o,`--childCount=${s}`]];return{proc:p(k,j,{cwd:e.root,detached:!0,env:l,stdio:["ignore","pipe","pipe"]}),launcher:"nx"}},"startServiceTree"),oe=new WeakMap,J=a((e,r,o)=>{const s=`${e.project}:${e.decl.serveTarget??"serve-hmr"}`,l=Math.max(1,e.decl.minPods??1),{proc:n,launcher:d}=re(e,r,s,l);oe.set(n,d),o==="active"&&(e.launcher=d),o==="active"&&(e.lastErrors=[],e.buildFailed=!1,e.faults=new pod_output_1.PodFaults);const u=a(j=>{const v=j.toString();for(const f of v.split(`
|
|
3
|
+
`)){const R=(0,pod_output_1.classifyPodLine)(f),{clean:M,broken:G,healthy:ne}=R,h=M;if(G){e.lastErrors.push(M),e.lastErrors.length>12&&e.lastErrors.shift(),e.buildFailed=!0;const x=Date.now();(M!==e.lastErrorEmitSig||x-(e.lastErrorEmitAt??0)>=types_1.SERVICE_ERROR_THROTTLE_MS)&&(e.lastErrorEmitSig=M,e.lastErrorEmitAt=x,S(e,"error",`service error: ${e.project}`,`cluster-service:${e.project}`,{kind:"service-error",project:e.project,line:M,fault:R.fault}))}ne&&(e.buildFailed=!1);const P=(e.faults??=new pod_output_1.PodFaults).observe(R);if(P){e.lastErrorEmitSig=void 0;const x=P.open.length===0;S(e,"info",`${x?"service recovered":`${P.closed.join(" + ")} recovered`}: ${e.project}`,`cluster-service:${e.project}`,{kind:x?"service-recovered":"service-fault-closed",project:e.project,line:M,closed:P.closed,open:P.open})}!e.lockBlockedAt&&/Waiting for .+ in another nx process/.test(h)&&(e.lockBlockedAt=Date.now(),console.log(`[gateway] ${s}: spawn blocked by a stale nx runner \u2014 breaking the lock + respawning`),S(e,"warning",`nx task lock: ${e.project} blocked by a stale runner \u2014 auto-breaking`,`cluster-service:${e.project}`,{kind:"nx-lock-break",project:e.project}),setTimeout(()=>{t.stopped()||!e.lockBlockedAt||(V(e,"parked on a foreign nx task lock \u2014 recycling",{announce:!1}),z(e,s),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&Y(e)},1500).unref?.())},250).unref?.())}},"sniff"),g=e.wsKey&&e.wsKey!==t.wsKey&&e.root?(0,workspace_log_1.workspaceLogWriter)(e.root):void 0,k=a(j=>{const v=(0,own_log_1.linePrefixer)(e.project);return f=>{if(t.stopped())return;const R=(0,inspector_secret_1.redactInspectorUrls)(f.toString());u(R);const M=v(R);j.write(M),g?.(M)}},"relay");return n.stdout?.on("data",k(process.stdout)),n.stderr?.on("data",k(process.stderr)),n.on("exit",(j,v)=>{if(e.proc!==n||(e.proc=void 0,e.state="down",t.stopped()))return;const f=(0,exit_reason_1.describeExit)(j,v);if(console.log(`[gateway] ${s} exited (${j??"no code"}${v?`, ${v}`:""}${e.inflight?`, ${e.inflight} request(s) in flight`:""})${f?` \u2014 ${f}`:""}`),S(e,j===0?"info":"warning",`service down: ${e.project} (exit ${j})`,`cluster-service:${e.project}`,{kind:"service-down",project:e.project,code:j,...v?{signal:v}:{}}),j===0||e.buildFailed)return;const R=Date.now();if(e.restarts=(e.restarts??[]).filter(G=>R-G<types_1.SERVICE_RESTART_WINDOW_MS),e.restarts.length>=types_1.SERVICE_MAX_RESTARTS){S(e,"error",`service crash-looping: ${e.project} \u2014 ${types_1.SERVICE_MAX_RESTARTS}+ restarts/min, not auto-respawning (the next request retries)`,`cluster-service:${e.project}`,{kind:"service-crash-loop",project:e.project});return}e.restarts.push(R);const M=Math.min(8e3,500*2**(e.restarts.length-1));setTimeout(()=>{t.stopped()||e.proc||e.state!=="down"||(console.log(`[gateway] self-heal: respawning ${e.project} (restart ${e.restarts.length}/${types_1.SERVICE_MAX_RESTARTS})`),S(e,"info",`self-heal: respawning ${e.project}`,`cluster-service:${e.project}`,{kind:"service-respawn",project:e.project,attempt:e.restarts.length}),Y(e))},M)}),n},"launchServiceProcess"),Y=a(e=>{if(t.stopped()||e.manualStopped)return;const r=`${e.project}:${e.decl.serveTarget??"serve-hmr"}`,o=Math.max(1,e.decl.minPods??1);i.reapForeign!==!1&&z(e,r),e.lockBlockedAt=void 0,console.log(`[gateway] starting ${r} (pods=${o})\u2026`),S(e,"info",`service starting: ${e.project} (${o} pod${o>1?"s":""})`,`cluster-service:${e.project}`,{kind:"service-starting",project:e.project,pods:o}),e.state="starting",e.spawnAt=Date.now(),e.generation=(e.generation??0)+1,e.proc=J(e,e.generation,"active")},"spawnService"),be=a(async e=>{const r=e.pool.socks[0];if(!r)return;const o=await(0,pod_probe_1.probePodAlive)(r,void 0,t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)]);o&&(e.activeWork=o.activeWork,o.watcher&&(e.watcher=o.watcher))},"refreshActiveWork"),Le=a(e=>{try{const r=Number(fs.readFileSync(`${e}.owner`,"utf8").trim());return Number.isInteger(r)&&r>1?r:void 0}catch{return}},"readOwnerSidecar"),Ie=a(async e=>{const r=t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)];if(!r)return[];const o=(0,node_crypto_1.createHash)("sha256").update(r).digest("hex").slice(0,8),s=[];for(const l of e.pool.socks){const n=await(0,pod_probe_1.probePodAlive)(l,void 0,r);n&&(!n.keyFingerprint||n.keyFingerprint!==o)&&s.push({sock:l,pid:n.pid})}return s},"staleAdoptedSocks"),Oe=a((e,r)=>{const o=r.map(s=>s.sock);for(const{sock:s,pid:l}of r){const n=chooseStalePodKillTarget(l,Le(s),process.pid);if(n!==void 0)try{process.kill(n,"SIGTERM")}catch{}try{fs.unlinkSync(s)}catch{}try{fs.unlinkSync(`${s}.owner`)}catch{}}e.pool.socks=e.pool.socks.filter(s=>!o.includes(s)),e.pool.idx=-1,e.pool.scannedAt=0},"evictStaleAdopted"),pe=a((e,r)=>{if(e.state==="up")return!1;e.state="up",e.wedgeRecycles=0;const o=e.pool.socks.length;return S(e,"info",`service up: ${e.project} (${o} pods, ${r})`,`cluster-service:${e.project}`,{kind:"service-up",project:e.project,pods:o,adopted:r==="adopted"}),!0},"markUp"),we=a(async e=>{if(e.manualStopped)return!1;if(O(e.pool)>0){const s=await Ie(e);if(e.manualStopped)return!1;if(s.length>0&&(console.error(`[gateway] ${e.project}: ${s.length} adopted pod(s) hold a foreign east-west key \u2014 evicting instead of adopting.`),S(e,"warning",`stale adoption: ${e.project} (foreign east-west key)`,`cluster-service:${e.project}`,{kind:"pod-recycle",project:e.project,reason:"stale-adoption",pods:s.length}),e.proc&&V(e,"stale adoption (foreign east-west key)"),Oe(e,s)),e.pool.socks.length>0)return pe(e,"adopted"),!0}if(t.stopped())return!1;let r=!1;const o=a(()=>e.state!=="starting"&&!e.proc,"notStarted");if(o())if(e.coldWait)await e.coldWait;else{E>=$&&(console.log(`[gateway] cold start queued: ${e.project}${e.wsKey&&e.wsKey!==t.wsKey?` (${e.wsKey})`:""} \u2014 ${E} starting, ${b.length} ahead (LENSMCP_MAX_COLD_STARTS=${$}).`),S(e,"info",`cold start queued: ${e.project}`,`cluster-service:${e.project}`,{kind:"cold-start-queued",project:e.project,inFlight:E,ahead:b.length}));const s=F();if(e.coldWait=s,await s,e.coldWait=void 0,r=!0,t.stopped()||e.manualStopped)return D(),!1;o()&&Y(e)}try{return await Ne(e)}finally{r&&D()}},"ensureUp"),Ne=a(async e=>{const r=Date.now()+t.startupTimeoutMs,o=Date.now()+12e3;for(;!t.stopped()&&!e.manualStopped&&Date.now()<r;){if(await sleep(Math.min(700,t.startupTimeoutMs/4)),e.manualStopped||e.buildFailed&&Date.now()>o)return!1;if(O(e.pool)>0){e.state="up",e.buildFailed=!1;const s=e.spawnAt?Date.now()-e.spawnAt:void 0;return console.log(`[gateway] ${e.project} up (${e.pool.socks.length} pods${s?`, cold start ${s}ms`:""})`),S(e,"info",`cold start: ${e.project} (${e.pool.socks.length} pods, ${s??"?"}ms)`,`cluster-service:${e.project}`,{kind:"cold-start",project:e.project,pods:e.pool.socks.length,durationMs:s}),!0}if(e.state==="down")return!1}return!1},"waitForPods"),Ke=a(e=>{const r=e.decl.maxPods??e.decl.minPods??1,o=e.pool.socks.length,s=scaleTarget(e);!s||o===0||o>=r||e.inflight<=o||Date.now()-e.lastScaleAt<5e3||(e.lastScaleAt=Date.now(),postScale(s,o+1,i.scaleLookup),console.log(`[gateway] autoscale ${e.project}: ${o} \u2192 ${o+1} pods (inflight ${e.inflight})`),S(e,"info",`autoscale: ${e.project} ${o} \u2192 ${o+1} pods`,`cluster-service:${e.project}`,{kind:"autoscale",project:e.project,from:o,to:o+1,inflight:e.inflight}))},"scaleUp"),ue=i.killGraceMs??types_1.TREE_KILL_GRACE_MS,N=new Map,Ce=a(e=>{const r=N.get(e);if(!r||(N.delete(e),clearTimeout(r.timer),!L(e)))return;const{svc:o,why:s}=r;console.log(`[gateway] ${o.project}: its tree (pid ${e.pid}) was still running ${Math.round(ue/1e3)}s after it was told to stop (${s}) \u2014 killing it (SIGKILL).`),S(o,"warning",`tree force-killed: ${o.project} (it ignored SIGTERM)`,`cluster-service:${o.project}`,{kind:"tree-force-killed",project:o.project,why:s,pid:e.pid,graceMs:ue}),T(e,"SIGKILL")},"settleDyingTree"),Se=a((e,r,o)=>{if(A(r,"SIGTERM"),ue<=0||N.has(r))return;const s=setTimeout(()=>Ce(r),ue);s.unref?.(),N.set(r,{svc:e,why:o,timer:s})},"stopTree"),Fe=a(async(e=3e3)=>{const r=Date.now()+e;for(;N.size>0;){for(const[o,s]of[...N])L(o)||(clearTimeout(s.timer),N.delete(o));if(N.size===0||Date.now()>=r)break;await sleep(100)}for(const o of[...N.keys()])Ce(o)},"finishDyingTrees"),V=a((e,r,o)=>{if(!e.proc)return;console.log(`[gateway] ${e.project} ${r} \u2014 scaling to zero.`),o?.announce!==!1&&S(e,"info",`idle kill: ${e.project} (${r})`,`cluster-service:${e.project}`,{kind:"idle-kill",project:e.project,why:r});const s=e.proc;try{s.stdout?.removeAllListeners("data"),s.stderr?.removeAllListeners("data"),s.removeAllListeners("exit")}catch{}(0,discovery_1.stopRoutingTo)(e.pool),Se(e,s,r),e.proc=void 0,e.state="down",setTimeout(()=>{ge(e),e.pool.socks=[],e.pool.scannedAt=0},1200).unref?.()},"killSpawned"),ge=a(e=>{try{for(const r of fs.readdirSync(e.pool.dir)){if(!r.endsWith(".sock"))continue;const o=path.join(e.pool.dir,r);try{const s=Number(fs.readFileSync(`${o}.owner`,"utf8").trim());if(Number.isFinite(s)&&s>0)try{process.kill(s,0);continue}catch{}}catch{}try{fs.unlinkSync(o)}catch{}try{fs.unlinkSync(`${o}.owner`)}catch{}}}catch{}},"sweepDeadOwnerSocks");let Q=0;const ve=[],We=a(()=>Q<X?(Q+=1,Promise.resolve()):new Promise(e=>{ve.push(()=>{Q+=1,e()})}),"acquireRollSlot"),Ge=a(()=>{Q-=1,ve.shift()?.()},"releaseRollSlot"),ye=a(async(e,r,o)=>{O(e.pool);for(const s of e.pool.socks){const l=await w(s,void 0,o);if(l&&r(l.podGeneration))return!0}return!1},"anyPodAnswers"),fe=new Map,qe=a((e,r,o)=>{const s=C>0&&(0,handoff_retire_1.requestHandoffRetire)(e.pool.dir,o);if(A(r,"SIGTERM"),!s)return;const l=e.wsKey&&e.wsKey!==t.wsKey&&e.root?(0,workspace_log_1.workspaceLogWriter)(e.root):void 0,n=a(ne=>h=>{if(t.stopped())return;const P=(0,inspector_secret_1.redactInspectorUrls)(h.toString()).replace(/^(?=.)/gm,`[${e.project} g${o} retiring] `);ne.write(P),l?.(P)},"say"),d=n(process.stdout),u=n(process.stderr);r.stdout?.on("data",d),r.stderr?.on("data",u);let g,k=!1;const j=a(()=>{k=!0,clearTimeout(g),r.stdout?.removeListener("data",d),r.stderr?.removeListener("data",u),r.removeListener("close",j)},"releaseLogs");r.once("close",j);let v=!1;const f=a(()=>{v||(v=!0,clearTimeout(R),clearInterval(G),t.stopped()?j():k||(g=setTimeout(j,1e3),g.unref?.()),fe.delete(r),(0,handoff_retire_1.clearHandoffRetire)(e.pool.dir,o),ge(e),e.pool.scannedAt=0)},"end"),R=setTimeout(()=>{console.error(`[gateway] pod handoff: ${e.project} g${o} was still running ${C}ms after it was retired \u2014 killing it.`),T(r,"SIGKILL"),f()},C);R.unref?.();const M=a(()=>{if(!v){if(t.stopped()){f();return}L(r)||(console.log(`[gateway] pod handoff: ${e.project} g${o} finished its in-flight work and exited.`),f())}},"look"),G=setInterval(M,ie);G.unref?.(),r.once("exit",M),fe.set(r,()=>{f(),Se(e,r,"the gateway is stopping")})},"retireReplacedTree"),Ve=i.standbyGraceMs??3e4,I=new Map,Ue=a((e,r,o,s,l)=>{const n=l+B+Ve,d=setTimeout(()=>Me(r),Math.max(0,n-Date.now()));d.unref?.(),I.set(r,{svc:e,generation:o,reason:s,beganAt:l,deadlineAt:n,timer:d})},"holdStandby"),Be=a(e=>{const r=I.get(e);r&&(clearTimeout(r.timer),I.delete(e))},"releaseStandby"),Z=a((e,r)=>{const o=I.get(e);if(!o)return!1;clearTimeout(o.timer),I.delete(e);const{svc:s,generation:l}=o;try{e.stdout?.removeAllListeners("data"),e.stderr?.removeAllListeners("data")}catch{}return(0,discovery_1.stopRoutingTo)(s.pool,n=>(0,pod_sock_1.podSockGeneration)(n)===l),Se(s,e,`standby g${l}: ${r}`),setTimeout(()=>ge(s),1200).unref?.(),!0},"endStandby"),Te=a((e,r)=>{const o=r.oldServing?`old generation g${r.fromGen} kept serving`:`and the old generation g${r.fromGen} is NOT serving (${r.oldGone?"its tree is gone":"none of its pods answers"}) \u2014 the service is down until it recovers`;console.error(`[gateway] pod handoff failed: ${e.project} g${r.generation}: ${r.why} \u2014 ${o}.`),e.rollFailure=`${r.why} \u2014 ${o}`,S(e,r.oldServing?"warning":"error",`pod handoff failed: ${e.project}`,`cluster-service:${e.project}`,{kind:"pod-handoff-failed",project:e.project,reason:r.reason,generation:r.generation,error:r.why,durationMs:Date.now()-r.beganAt,oldServing:r.oldServing})},"reportStandbyFailed"),Me=a(e=>{const r=I.get(e);if(!r)return;const{svc:o,generation:s,reason:l,beganAt:n}=r,d=`the standby outlived its roll (${Math.round((Date.now()-n)/1e3)}s; a roll waits ${Math.round(B/1e3)}s for it)`;Z(e,d);const u=o.proc,g=t.serviceKeys[(0,service_keys_1.serviceKeyId)(o.wsKey,o.project)];(async()=>{const k=!u||u.exitCode!==null||u.signalCode!==null,j=k?!1:await Promise.race([ye(o,f=>f!==s,g).catch(()=>!1),sleep(2e3).then(()=>{})]),v=!t.stopped()&&!k&&(j??O(o.pool)>0);Te(o,{reason:l,generation:s,fromGen:o.generation??s-1,why:d,beganAt:n,oldServing:v,oldGone:k})})()},"outlivedRoll"),He=a(()=>{for(const[e,r]of[...I])Date.now()>=r.deadlineAt?Me(e):!r.svc.rolling&&r.svc.proc!==e&&(console.error(`[gateway] pod handoff: ${r.svc.project} g${r.generation} is a standby whose roll is over \u2014 stopping it.`),Z(e,"its roll is over"))},"sweepStandbys"),Ye=a(()=>{for(const e of[...fe.values()])e();for(const e of[...I.keys()])Z(e,"the gateway is stopping")},"stopRetiringTrees"),me=a((e,r)=>{if(e.rolling)return e.rolling;const o=e.proc;if(!o||t.stopped())return Promise.resolve(!1);if(e.launcher==="nx")return console.log(`[gateway] pod handoff skipped: ${e.project} runs through nx run, which starts one instance of a target at a time \u2014 no standby can overlap it (reason=${r}).`),Promise.resolve(!1);Q>=X&&console.log(`[gateway] pod handoff queued: ${e.project} reason=${r} \u2014 ${Q} roll(s) in flight, ${ve.length} ahead (LENSMCP_POD_MAX_CONCURRENT_ROLLS=${X}).`);const s=(async()=>{await We();try{return await Xe(e,o,r)}finally{Ge()}})().finally(()=>{e.rolling=void 0});return e.rolling=s,s},"rollPod"),Xe=a(async(e,r,o)=>{const s=e.proc;if(s!==r||t.stopped())return!1;const l=e.generation??0,n=l+1,d=Date.now();e.lastStaleRecycleAt=d,console.log(`[gateway] pod handoff start: ${e.project} g${l}\u2192g${n} reason=${o} \u2014 standby spawning, old generation keeps serving.`),S(e,"info",`pod handoff starting: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"pod-handoff-start",project:e.project,reason:o,generation:n,fromGeneration:l});const u=J(e,n,"standby"),g=t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)];Ue(e,u,n,o,d);let k=!1;try{const j=d+B;let v;for(;!t.stopped()&&Date.now()<j&&I.has(u)&&!(u.exitCode!==null||u.signalCode!==null);){O(e.pool);for(const f of e.pool.socks)if((await w(f,void 0,g))?.podGeneration===n){v=f;break}if(v)break;await sleep(le)}if(!I.has(u))return!1;if(!v||t.stopped()){const f=t.stopped()?"gateway stopping":u.exitCode!==null||u.signalCode!==null?`standby exited (${u.exitCode??u.signalCode})`:`standby not ready within ${Math.round(B/1e3)}s`;Z(u,f);const R=!t.stopped()&&e.proc===s&&s.exitCode===null&&s.signalCode===null&&await ye(e,M=>M!==n,g);return Te(e,{reason:o,generation:n,fromGen:l,why:f,beganAt:d,oldServing:R,oldGone:e.proc!==s||s.exitCode!==null||s.signalCode!==null}),!1}if(e.proc!==s)return Z(u,"the service was replaced underneath the roll"),!1;Be(u),k=!0,e.proc=u,e.launcher=oe.get(u),e.generation=n,e.spawnAt=d,e.state="up",e.watcher=void 0,e.staleVerifiedAt=void 0,e.staleKeptAt=void 0,e.buildFailed=!1,e.lastErrors=[];try{s.stdout?.removeAllListeners("data"),s.stderr?.removeAllListeners("data"),s.removeAllListeners("exit")}catch{}return(0,discovery_1.stopRoutingTo)(e.pool,f=>(0,pod_sock_1.podSockGeneration)(f)!==n&&path.join(e.pool.dir,f)!==v),qe(e,s,l),setTimeout(()=>{ge(e),e.pool.scannedAt=0},1200).unref?.(),console.log(`[gateway] pod handoff ready: ${e.project} g${n} answered on ${path.basename(v)} in ${Date.now()-d}ms; retiring g${l}.`),S(e,"info",`pod handoff complete: ${e.project}`,`cluster-service:${e.project}`,{kind:"pod-handoff-complete",project:e.project,reason:o,generation:n,fromGeneration:l,durationMs:Date.now()-d}),!0}finally{k||Z(u,"its roll ended without it")}},"runRoll"),ze=a((e,r)=>{if(t.stopped())return{refused:"the gateway is stopping"};if(e.manualStopped=!1,e.actionFailure=void 0,e.restart&&!e.restart.done)return{restart:e.restart,already:!0};if(!e.proc&&e.state!=="down")return{refused:`${e.project} is served by pods this gateway adopted from an earlier one \u2014 it has no tree of its own to replace. \`lensmcp gateway restart\` starts every service under this gateway.`};const o=e.proc?r.hard||e.launcher==="nx"||e.state!=="up"?"hard":"rolling":"start",s={how:o,reason:r.reason,startedAt:Date.now(),...e.proc&&e.generation!==void 0?{fromGeneration:e.generation}:{}};return e.restart=s,e.lastUsed=s.startedAt,console.log(`[gateway] restart asked: ${e.project}${e.wsKey!==t.wsKey?` (${e.wsKey})`:""} \u2014 ${o} (${r.reason}).`),S(e,"info",`restart asked: ${e.project} (${o})`,`cluster-service:${e.project}`,{kind:"service-restart",phase:"started",project:e.project,how:o,reason:r.reason,...s.fromGeneration!==void 0?{fromGeneration:s.fromGeneration}:{}}),a(async()=>{if(o==="start")return await we(e)?{ok:!0}:{ok:!1,error:$e(e)};if(o==="rolling")return e.rolling&&await e.rolling,e.proc?(e.rollFailure=void 0,await me(e,r.reason)?{ok:!0}:{ok:!1,error:e.rollFailure??"the new tree did not take over"}):await we(e)?{ok:!0}:{ok:!1,error:$e(e)};if(V(e,`restart asked (${r.reason})`,{announce:!1}),await sleep(1500),t.stopped())return{ok:!1,error:"the gateway is stopping"};let l=!1;e.proc||(await F(),l=!0,!e.proc&&e.state!=="starting"&&Y(e));try{const n=e.proc,d=e.generation,u=t.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)],g=Date.now()+t.startupTimeoutMs;for(;!t.stopped()&&Date.now()<g;){if(!n||e.proc!==n||n.exitCode!==null||n.signalCode!==null)return{ok:!1,error:$e(e)};if(await ye(e,k=>k===d,u))return pe(e,"observed"),e.buildFailed=!1,{ok:!0};await sleep(le)}return{ok:!1,error:t.stopped()?"the gateway is stopping":`no pod of the new tree answered within ${Math.round(t.startupTimeoutMs/1e3)}s`}}finally{l&&D()}},"run")().catch(l=>({ok:!1,error:l.message})).then(l=>{s.done={ok:l.ok,at:Date.now(),...e.generation!==void 0?{generation:e.generation}:{},...l.error?{error:l.error}:{}},l.ok&&(e.lastUsed=s.done.at);const n=Math.round((s.done.at-s.startedAt)/100)/10;l.ok?console.log(`[gateway] restarted: ${e.project} \u2014 g${s.done.generation??"?"} serving after ${n}s (${o}).`):console.error(`[gateway] restart failed: ${e.project} \u2014 ${l.error} (${o}, after ${n}s).`),S(e,l.ok?"info":"error",l.ok?`restarted: ${e.project}`:`restart failed: ${e.project}`,`cluster-service:${e.project}`,{kind:"service-restart",phase:l.ok?"done":"failed",project:e.project,how:o,reason:r.reason,...s.fromGeneration!==void 0?{fromGeneration:s.fromGeneration}:{},...s.done.generation!==void 0?{generation:s.done.generation}:{},durationMs:s.done.at-s.startedAt,...l.error?{error:l.error}:{}})}),{restart:s,already:!1}},"restartService"),$e=a(e=>e.buildFailed?`its build failed${e.lastErrors?.length?`: ${e.lastErrors[e.lastErrors.length-1]}`:""}`:e.proc&&(e.proc.exitCode!==null||e.proc.signalCode!==null)?`its tree exited (${e.proc.exitCode??e.proc.signalCode})`:!e.proc&&e.state==="down"?"its tree exited before a pod answered":`no pod answered within ${Math.round(t.startupTimeoutMs/1e3)}s`,"startFailure"),xe=a(e=>{const r=Date.now();e.restarts=(e.restarts??[]).filter(s=>r-s<types_1.SERVICE_RESTART_WINDOW_MS);const o=(e.wedgeRecycles??0)+1;if(e.restarts.length>=types_1.SERVICE_MAX_RESTARTS||o>types_1.SERVICE_MAX_RESTARTS){const s=e.restarts.length>=types_1.SERVICE_MAX_RESTARTS?`wedged ${types_1.SERVICE_MAX_RESTARTS}+\xD7/min`:`recycled ${types_1.SERVICE_MAX_RESTARTS}\xD7 in a row and no pod came up`;S(e,"error",`service crash-looping: ${e.project} \u2014 ${s}, not auto-recycling (the next request retries)`,`cluster-service:${e.project}`,{kind:"service-crash-loop",project:e.project,recycles:o-1}),V(e,"wedged (crash-loop cap) \u2014 leaving down for the next request",{announce:!1});return}e.wedgeRecycles=o,e.restarts.push(r),console.log(`[gateway] self-heal: recycling WEDGED ${e.project} (live parent, 0 live pods; attempt ${o}/${types_1.SERVICE_MAX_RESTARTS} with no pod up since the first, ${e.restarts.length} in the last minute; ${e.inflight??0} request(s) in flight \u2014 jobs held by the dead pod keep their locks until they expire)`),S(e,"warning",`self-heal: recycling wedged ${e.project} (0 live pods)`,`cluster-service:${e.project}`,{kind:"service-recycle",project:e.project,attempt:o,inWindow:e.restarts.length}),V(e,"wedged: 0 live pods \u2014 recycling",{announce:!1}),setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&Y(e)},1500).unref?.()},"recycleWedged"),Je={maxAgeMs:types_1.POD_MAX_AGE_MS,graceMs:types_1.POD_STALE_GRACE_MS,settleMs:types_1.POD_RECYCLE_SETTLE_MS,cooldownMs:types_1.POD_STALE_RECYCLE_COOLDOWN_MS};let _e;const se=new Map,Ee=new Map;let ke=!1,je=0,he=0;const Qe=a(()=>{const e=t.projectRoots??{},r=new Set(t.services.map(s=>s.project));for(const s of t.routes)s.lens&&r.add(s.lens.project);const o=new Set;for(const s of r){const l=e[s];if(!l)continue;const n=path.relative(t.root,l).split(path.sep).filter(Boolean)[0];n&&!n.startsWith("..")&&o.add(n)}return[...o]},"managedBuckets"),Ze=a((e,r,o,s)=>{const l=t.projectSourceRoots?.[e];if(l?.length)return l;const n=t.projectRoots?.[e];if(!n)return;const d=sourceScopeDirs(t.root,n,o,r,s);return d.length>0?d:void 0},"scopeFor"),et=a(()=>{if(ke||Date.now()-je<types_1.POD_STALE_SCAN_MS)return;ke=!0,je=Date.now();const e=je;(async()=>{const r=(0,scope_1.readSourceSetOptions)(t.root),o=r.exclude,s={ignoreTests:r.ignoreTests},l=sourceScanPass(t.root,o,s),n=await l.snapshot(),d=Date.now(),u=sourceChangeWindowMs(d,he,types_1.POD_STALE_SCAN_MS,types_1.POD_RECYCLE_SETTLE_MS),g=_e?diffSourceSets(_e,n,d,u):null;if(g&&(t.sourceSetChangedAt=d,t.sourceSetChange=g,H("info","source file set changed \u2014 stale pods recycle when idle","cluster-gateway",{kind:"source-set-changed",added:g.added,removed:g.removed,truncated:g.truncated})),_e=n,!t.projectRoots){he=e;return}let k;try{k=(await fs.promises.readdir(t.root,{withFileTypes:!0})).filter(h=>h.isDirectory()).map(h=>h.name)}catch{return}const j=Qe(),v=new Set(t.services.map(h=>h.project));for(const h of t.routes)h.lens&&v.add(h.lens.project);const f=new Map;for(const h of v){const P=Ze(h,k,j,o);if(!P)continue;const x=P.join("|"),q=f.get(x)??{dirs:P,projects:[]};q.projects.push(h),f.set(x,q)}const R=Date.now(),M=sourceChangeWindowMs(R,he,types_1.POD_STALE_SCAN_MS,types_1.POD_RECYCLE_SETTLE_MS);he=e;const G=a((h,P,x)=>{t.sourceSetChangedFor??=new Map,t.sourceSetChangesFor??=new Map;for(const q of h)t.sourceSetChangedFor.set(q,P),t.sourceSetChangesFor.set(q,x)},"stampScoped"),ne=new Set([...f.values()].flatMap(h=>h.projects));for(const h of[...t.sourceSetScoped??[]])ne.has(h)||t.sourceSetScoped.delete(h);for(const[h,{dirs:P,projects:x}]of f){const q=await l.snapshot(P),Re=se.get(h);if(Re||x.some(K=>Ee.has(K))){t.sourceSetScoped??=new Set;for(const K of x)t.sourceSetScoped.add(K)}se.set(h,q);const Ae=new Map;for(const K of x){const U=Ee.get(K);if(U===void 0||U===h)continue;const ee=Ae.get(U)??[];ee.push(K),Ae.set(U,ee)}for(const K of x)Ee.set(K,h);for(const[K,U]of Ae){const ee=se.get(K),ce=ee?diffSourceSets(ee,q,R,M):g;ce&&(G(U,ee?R:d,ce),H("info",`source file set changed \u2014 ${U.join(", ")}'s watch scope was re-keyed`,"cluster-gateway",{kind:"source-set-changed",scoped:!0,rescoped:!0,projects:U,added:ce.added,removed:ce.removed,truncated:ce.truncated}))}if(!Re)continue;const ae=diffSourceSets(Re,q,R,M);ae&&(G(x,R,ae),H("info",`source file set changed in ${x.join(", ")}'s scope`,"cluster-gateway",{kind:"source-set-changed",scoped:!0,projects:x,added:ae.added,removed:ae.removed,truncated:ae.truncated}))}for(const h of[...se.keys()])f.has(h)||se.delete(h)})().catch(()=>{}).finally(()=>{ke=!1})},"maybeScanSourceSet"),tt=a((e,r)=>{const o=podChangeClock(t,e.project),s=o.change,l=`${o.scope==="project"?"in its scope":"in the workspace (its scope is not known)"}: ${changeEvidence(s)}`,n=s?.windowMs??types_1.POD_STALE_SCAN_MS+types_1.POD_RECYCLE_SETTLE_MS,d=podWatcherVerdict(e.watcher,r,n),u=a(k=>(e.staleKeptAt!==r&&(e.staleKeptAt=r,console.log(`[gateway] stale source set: pod ${e.project} ${k} \u2014 no recycle.`)),!1),"keep");if(d==="heard")return e.staleVerifiedAt=r,S(e,"info",`stale check: ${e.project} watcher confirmed \u2014 no recycle`,`cluster-service:${e.project}`,{kind:"pod-stale-verified",project:e.project,watcher:d}),u(`watcher CONFIRMED alive across the change (last build ${new Date(e.watcher?.lastBuildAt??0).toISOString()})`);if(d==="degraded")return console.log(`[gateway] stale source set: pod ${e.project} watcher DEGRADED (${e.watcher?.degraded}) \u2014 recycling.`),!0;const g=a(k=>(e.staleKeptAt!==r&&(e.staleKeptAt=r,console.log(`[gateway] stale source set: pod ${e.project} ${k}; the change ${l} \u2014 recycling.`)),!0),"recycling");return d==="silent"?types_1.POD_WATCHER_SILENT==="keep"?u("watcher alive but quiet across the change \u2014 keeping it (LENSMCP_POD_WATCHER_SILENT=keep; a live watcher cannot miss an add it will need)"):g("watcher alive but quiet across the change"):types_1.POD_WATCHER_UNVERIFIED==="keep"?u("unverifiable (no watcher testimony \u2014 older devserver bundle?) \u2014 keeping it (LENSMCP_POD_WATCHER_UNVERIFIED=keep)"):g("watcher unverifiable (no testimony yet \u2014 a pod that never rebuilt, or an older devserver bundle)")},"podStaleRecycleAllowed"),rt=a((e,r)=>{if(e.lastStaleRecycleAt=Date.now(),types_1.POD_RECYCLE_MODE==="rolling"&&e.proc&&e.launcher!=="nx"){me(e,r);return}console.log(`[gateway] zombie recycle: ${e.project} (${r}) \u2014 stale fs view; fresh pod incoming`),S(e,"warning",`zombie recycle: ${e.project} (${r})`,`cluster-service:${e.project}`,{kind:"pod-recycle",project:e.project,reason:r});const o=!!e.decl.eager;V(e,`${r} \u2014 recycling stale pod`,{announce:!1}),o&&setTimeout(()=>{!t.stopped()&&!e.proc&&e.state==="down"&&Y(e)},1500).unref?.()},"recyclePod"),ot=a(()=>{const e=setInterval(()=>{ct().catch(r=>console.error("[gateway] sweep error:",r))},t.sweepMs);return e.unref(),e},"startSweeper"),st=i.listProcesses??(()=>{try{return(0,orphan_reaper_1.parsePsRows)((0,node_child_process_1.execFileSync)("ps",orphan_reaper_1.PS_ROW_ARGS,{encoding:"utf8",timeout:5e3}))}catch{return[]}}),Pe=i.signalProcess??((e,r)=>{process.kill(e,r)});let De=0;const nt=a(()=>{if(typeof process.getuid!="function")return 0;const e=t.registry?t.registry.allServices():t.services;if(e.length===0)return 0;const r=e.map(n=>({label:n.wsKey||path.basename(n.root),project:n.project,root:n.root,svc:n})),o=new Set;for(const n of e)n.proc?.pid&&o.add(n.proc.pid);for(const n of[...fe.keys(),...N.keys(),...I.keys()])n.pid&&o.add(n.pid);const s=[...new Set((0,process_name_1.defaultLinkDirs)().flatMap(n=>{try{return[n,fs.realpathSync(n)]}catch{return[n]}}))];let l=0;for(const n of(0,orphan_reaper_1.findOrphanExecutors)(st(),r,{linkDirs:s,uid:process.getuid(),selfPid:process.pid,ownGroups:o})){const{row:d}=n;try{Pe(n.group?-d.pgid:d.pid,"SIGKILL")}catch(g){if(g.code!=="ESRCH"||!n.group)continue;try{Pe(d.pid,"SIGKILL")}catch{continue}}l+=1;const u=n.owner.svc;console.log(`[gateway] reaped an orphaned executor of ${u.project} (${n.role}, pid ${d.pid}${n.group?`, group ${d.pgid}`:""}, parent gone) \u2014 a stopped tree it outlived: ${d.command.slice(0,160)}`),S(u,"warning",`orphan reap: ${u.project} (pid ${d.pid})`,`cluster-service:${u.project}`,{kind:"orphan-executor-reap",project:u.project,pid:d.pid,pgid:d.pgid,role:n.role,group:n.group})}return l},"reapOrphanExecutors"),ct=a(async()=>{{if(t.stopped())return;He(),i.reapForeign!==!1&&types_1.ORPHAN_REAP_EVERY_MS>0&&Date.now()-De>=types_1.ORPHAN_REAP_EVERY_MS&&(De=Date.now(),nt());for(const e of t.graphStores?.all()??[])e.refreshIfStale();types_1.POD_RECYCLE_ENABLED&&et();for(const e of t.registry?t.registry.allServices():t.services){if(t.stopped()||!e.proc){e.wedgedSince=void 0;continue}const r=path.resolve(e.root)===path.resolve(t.root),o=(0,warm_start_1.serviceIdleSec)(t,e),s=(0,awake_1.idleStopAt)(e,o);if(s!==void 0&&e.state==="up"&&(!e.restart||e.restart.done)&&e.inflight===0&&Date.now()>s){e.lastIdleStop={at:Date.now(),why:`no request for ${o}s${(e.awakeUntil??0)>e.lastUsed?" after its hold ended":""}`},V(e,`idle ${o}s`),e.wedgedSince=void 0;continue}const l=e.state==="starting"&&e.spawnAt!==void 0&&Date.now()-e.spawnAt<t.startupTimeoutMs,n=O(e.pool)>0;if(n||l){if(e.wedgedSince=void 0,n&&pe(e,"observed"),n&&!l&&types_1.POD_RECYCLE_ENABLED&&r){const d=podChangeClock(t,e.project).at;await be(e);const u=(e.staleVerifiedAt??0)>=d&&d>0,g=podRecycleReason(e,Date.now(),u?0:d,Je);if(g==="stale-source-set"&&!tt(e,d))continue;g&&rt(e,g)}continue}e.wedgedSince??=Date.now(),Date.now()-e.wedgedSince>types_1.WEDGE_RECYCLE_GRACE_MS&&(e.wedgedSince=void 0,xe(e))}}},"sweepOnce");return{scanNow:O,spawnService:Y,ensureUp:we,scaleUp:Ke,killSpawned:V,markUp:pe,emitSvc:S,startSweeper:ot,rollPod:me,restartService:ze,recycleWedged:xe,stopRetiringTrees:Ye,finishDyingTrees:Fe,stopBuildHost:a(e=>e===void 0?_.stop():_.release(e),"stopBuildHost")}}m(createServiceLayer,"createServiceLayer"),a(createServiceLayer,"createServiceLayer");function chooseStalePodKillTarget(t,c,i){if(!(t===void 0||!Number.isInteger(t)||t<=1)&&!(c!==void 0&&c!==t)&&t!==i)return t}m(chooseStalePodKillTarget,"chooseStalePodKillTarget"),a(chooseStalePodKillTarget,"chooseStalePodKillTarget");
|
|
@@ -25,9 +25,14 @@ export interface Observability {
|
|
|
25
25
|
*/
|
|
26
26
|
startHeadlessFlow(req: http.IncomingMessage, route: Route, trace: GwTrace | undefined, receivedAt: number): GwTrace | undefined;
|
|
27
27
|
traceStep(t: GwTrace | undefined, stepName: string, info?: Record<string, unknown>): void;
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
/**
|
|
29
|
+
* `route` identifies the OWNING workspace, so a guest's request narration goes to its own bus. `status` is what the
|
|
30
|
+
* caller was SENT — `undefined` when it went away before any response head (foodguard-118): the span then reads
|
|
31
|
+
* «no response», never the 200 an unsent response defaults to.
|
|
32
|
+
*/
|
|
33
|
+
finishTrace(t: GwTrace | undefined, project: string, status: number | undefined, extra?: Record<string, unknown>, route?: Route): void;
|
|
34
|
+
/** `status` `undefined`: the caller went away before any response — timed, but not an upstream error. */
|
|
35
|
+
recordEdge(route: Route, ms: number, status: number | undefined, caller?: string): void;
|
|
31
36
|
/** Start the periodic traffic-edge flush; returns the interval (already `.unref()`'d). */
|
|
32
37
|
startEdgeFlusher(): NodeJS.Timeout;
|
|
33
38
|
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"use strict";var
|
|
2
|
-
`),
|
|
3
|
-
`;busHygiene?.maybeRotateEventFile(n,h.length),E(n,h)}catch{}},"emit"),S=d(e=>{const
|
|
1
|
+
"use strict";var b=Object.defineProperty;var g=(s,u)=>b(s,"name",{value:u,configurable:!0});var D=Object.defineProperty,d=g((s,u)=>D(s,"name",{value:u,configurable:!0}),"d");Object.defineProperty(exports,"__esModule",{value:!0}),exports.EDGE_DURATION_SAMPLES=exports.LENS_DASHBOARD_PROJECT=void 0,exports.createObservability=createObservability;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),path=tslib_1.__importStar(require("node:path")),headless_flow_1=require("@lensmcp/core/headless-flow"),http_exchange_1=require("@lensmcp/core/http-exchange"),redact_1=require("@lensmcp/core/redact"),busHygiene=(()=>{try{return require("@lensmcp/node-instrumentation")}catch{return}})();exports.LENS_DASHBOARD_PROJECT="(lens-dashboard)",exports.EDGE_DURATION_SAMPLES=64;function ownerOf(s){return s?.svc?{root:s.svc.root,wsKey:s.svc.wsKey}:s?.lens?{...s.lens.workspaceRoot?{root:s.lens.workspaceRoot}:{},...s.lens.workspaceKey?{wsKey:s.lens.workspaceKey}:{}}:{}}g(ownerOf,"ownerOf"),d(ownerOf,"ownerOf");function busFileFor(s,u){const{root:l}=ownerOf(s);if(!l)return;const w=path.join(l,".lensmcp","events.jsonl");return w===u?void 0:w}g(busFileFor,"busFileFor"),d(busFileFor,"busFileFor");const hdr=d(s=>typeof s=="string"&&s!==""?s:Array.isArray(s)?s[0]:void 0,"hdr"),lensRequestId=d(s=>hdr(s.headers[http_exchange_1.LENS_REQUEST_ID_HEADER])??hdr(s.headers["x-request-id"]),"lensRequestId");function createObservability(s,u){const l=d(()=>Date.now().toString(36)+Math.random().toString(36).slice(2,12),"eid"),w=busHygiene?.createEventFloodGuard({burst:30,perSec:5}),f=new Map,E=d((e,r)=>{try{const t=f.get(e);t&&(fs.appendFileSync(e,JSON.stringify({id:l(),sessionId:"pending",timestamp:Date.now(),source:"gateway",category:"cluster",severity:"warning",context:{sessionId:"pending"},fingerprint:"cluster-gateway:bus-dropped",title:`lens bus: ${t.count} event${t.count===1?"":"s"} could not be written since ${new Date(t.since).toISOString()} (${t.code}) \u2014 they are lost`,raw:{kind:"bus-events-dropped",project:"gateway",dropped:t.count,since:t.since,until:Date.now(),code:t.code}})+`
|
|
2
|
+
`),f.delete(e),console.warn(`[gateway] the lens bus ${e} takes events again \u2014 ${t.count} were lost (${t.code})`)),fs.appendFileSync(e,r)}catch(t){const a=f.get(e);if(a){a.count+=1;return}const o=t?.code??"EIO";f.set(e,{count:1,since:Date.now(),code:o}),console.warn(`[gateway] the lens bus ${e} cannot be written (${o}) \u2014 counting the events lost until it can`)}},"appendCounted"),y=d((e,r,t,a,o,i)=>{const n=i??s;try{const c=w?w.admit(t):{ok:!0,suppressed:0};if(!c.ok)return;fs.mkdirSync(path.dirname(n),{recursive:!0});const p=a&&(0,redact_1.redactDeep)(a),h=JSON.stringify({id:l(),sessionId:"pending",timestamp:Date.now(),source:"gateway",category:"cluster",severity:e,context:(0,redact_1.redactDeep)({sessionId:"pending",...o}),fingerprint:t,title:(0,redact_1.safeTitle)(r),raw:c.suppressed>0?{...p,floodSuppressed:c.suppressed}:p})+`
|
|
3
|
+
`;busHygiene?.maybeRotateEventFile(n,h.length),E(n,h)}catch{}},"emit"),S=d(e=>{const r=hdr(e.headers["x-lensmcp-flow-id"]);let t=lensRequestId(e);if(!(!r&&!t&&!hdr(e.headers.traceparent)))return t||(t=l(),e.headers["x-request-id"]=t),{startedAt:Date.now(),method:(e.method??"GET").toUpperCase(),host:(e.headers.host||"").split(":")[0],url:e.url??"/",steps:[],...r?{flowId:r}:{},...t?{requestId:t}:{},...hdr(e.headers["x-lensmcp-parent-request-id"])?{parentRequestId:hdr(e.headers["x-lensmcp-parent-request-id"])}:{},...hdr(e.headers["x-lensmcp-origin-node-id"])?{originNodeId:hdr(e.headers["x-lensmcp-origin-node-id"])}:{}}},"beginTrace"),v=d((e,r,t,a)=>{if(t?.flowId||!r.pool||r.internal||r.project===exports.LENS_DASHBOARD_PROJECT||!(0,headless_flow_1.headlessFlowsEnabled)())return t;const o=e.url??"/";if((0,headless_flow_1.isProbePath)(o))return t;const i=lensRequestId(e);if(!i)return t;const n=(0,headless_flow_1.headlessFlowId)(i);e.headers["x-lensmcp-flow-id"]=n;const c=(e.method??"GET").toUpperCase(),p=(e.headers.host||"").split(":")[0],h=`request:${c} ${o.split("?")[0]}`,I=t??{startedAt:a,method:c,host:p,url:o,steps:[{step:"received",atMs:0,host:p,method:c,url:o}],requestId:i};return I.flowId=n,I.originNodeId=h,y("info",`${c} ${o.split("?")[0]}`,`gateway-origin:${r.project}`,{kind:"flow-origin",originNodeId:h,project:r.project,host:p,method:c,url:o,...ownerOf(r).wsKey?{wsKey:ownerOf(r).wsKey}:{}},{flowId:n,requestId:i,originType:"request",originNodeId:h},busFileFor(r,s)),I},"startHeadlessFlow"),_=d((e,r,t)=>{e&&e.steps.push({step:r,atMs:Date.now()-e.startedAt,...t})},"traceStep"),$=d((e,r,t,a,o)=>{if(!e||e.done||(e.done=!0,r===exports.LENS_DASHBOARD_PROJECT&&(t===void 0||t<500)))return;const i=Date.now()-e.startedAt;y(t===void 0?"warning":t>=500?"error":"info",t===void 0?`gateway ${e.method} ${e.host}${e.url} \u2192 no response: the caller went away after ${i}ms`:`gateway ${e.method} ${e.host}${e.url} \u2192 ${t} (${i}ms)`,`gateway-request:${r}`,{kind:"gateway-request",project:r,host:e.host,method:e.method,url:e.url,...t===void 0?{}:{status:t},durationMs:i,startedAt:e.startedAt,steps:e.steps,...ownerOf(o).wsKey?{wsKey:ownerOf(o).wsKey}:{},...a},{...e.flowId?{flowId:e.flowId}:{},...e.requestId?{requestId:e.requestId}:{},...e.parentRequestId?{parentRequestId:e.parentRequestId}:{},...e.originNodeId?{originNodeId:e.originNodeId}:{}},busFileFor(o,s))},"finishTrace"),m=new Map;return{emit:y,eid:l,beginTrace:S,startHeadlessFlow:v,traceStep:_,finishTrace:$,recordEdge:d((e,r,t,a)=>{if(e.project===exports.LENS_DASHBOARD_PROJECT&&(t===void 0||t<500&&t!==0))return;const o=e.host??"(default)",i=(a?a+"\u21D2":"")+o+"\u2192"+e.project,n=m.get(i)??{host:o,project:e.project,caller:a,count:0,totalMs:0,errors:0,durations:[],...busFileFor(e,s)?{busFile:busFileFor(e,s)}:{},...ownerOf(e).wsKey?{wsKey:ownerOf(e).wsKey}:{}};if(n.count+=1,n.totalMs+=r,n.durations.length<exports.EDGE_DURATION_SAMPLES)n.durations.push(Math.round(r));else{const c=Math.floor(Math.random()*n.count);c<exports.EDGE_DURATION_SAMPLES&&(n.durations[c]=Math.round(r))}t!==void 0&&(t>=500||t===0)&&(n.errors+=1),m.set(i,n)},"recordEdge"),startEdgeFlusher:d(()=>{const e=setInterval(()=>{for(const r of m.values()){const t=Math.round(r.totalMs/r.count);y(r.errors?"warning":"info",`traffic ${r.host} \u2192 ${r.project}: ${r.count} req, ~${t}ms${r.errors?`, ${r.errors} errors`:""}`,`gateway-traffic:${r.host}->${r.project}`,{kind:"gateway-traffic",host:r.host,project:r.project,caller:r.caller,count:r.count,avgMs:t,errors:r.errors,durationsMs:r.durations,...r.wsKey?{wsKey:r.wsKey}:{}},void 0,r.busFile)}m.clear()},u.trafficFlushMs);return e.unref(),e},"startEdgeFlusher")}}g(createObservability,"createObservability"),d(createObservability,"createObservability");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var ee=Object.defineProperty;var D=(o,c)=>ee(o,"name",{value:c,configurable:!0});var Z=Object.defineProperty,s=D((o,c)=>Z(o,"name",{value:c,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.isReplayableRequest=isReplayableRequest,exports.upstreamOpen=upstreamOpen,exports.wedgedMessage=wedgedMessage,exports.createProxy=createProxy;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),edge_1=require("./edge"),gateway_errors_1=require("../gateway-errors"),body_stall_1=require("../body-stall"),types_1=require("./types"),pod_probe_1=require("./pod-probe"),gc_notice_1=require("../../../gc-notice"),service_keys_1=require("./service-keys");function isConnError(o){const c=o.code??"";return c==="ECONNREFUSED"||c==="ECONNRESET"||c==="ETIMEDOUT"||c==="EPIPE"||c==="EHOSTUNREACH"||c==="ENOENT"||/ECONNREFUSED|ECONNRESET|socket hang up|EPIPE/i.test(o.message)}D(isConnError,"isConnError"),s(isConnError,"isConnError");const IDEMPOTENT_METHODS=new Set(["GET","HEAD","OPTIONS","PUT","DELETE","TRACE"]);function isReplayableRequest(o){if(!IDEMPOTENT_METHODS.has((o.method??"GET").toUpperCase())||o.headers["transfer-encoding"]!==void 0)return!1;const c=o.headers["content-length"];return!(c!==void 0&&String(c).trim()!=="0")}D(isReplayableRequest,"isReplayableRequest"),s(isReplayableRequest,"isReplayableRequest");function edgeGone(o,c){return c.headersSent||c.writableEnded||c.destroyed===!0||o.aborted===!0}D(edgeGone,"edgeGone"),s(edgeGone,"edgeGone");const HOP_BY_HOP=new Set(["connection","keep-alive","proxy-connection","proxy-authenticate","proxy-authorization","te","trailer","transfer-encoding","upgrade"]),UPSTREAM_CONNECTION_HEADERS=["connection","keep-alive","proxy-connection"];function returnRequestId(o,c){if(o["x-request-id"]!==void 0)return;const u=c.headers["x-request-id"],q=Array.isArray(u)?u[0]:u;typeof q=="string"&&q!==""&&(o["x-request-id"]=q)}D(returnRequestId,"returnRequestId"),s(returnRequestId,"returnRequestId");const httpProxy=require("http-proxy");function declaredFirstByteMs(o){const c=Number(o);return Number.isFinite(c)&&c>0?c:types_1.POD_RESPONSE_TIMEOUT_MS}D(declaredFirstByteMs,"declaredFirstByteMs"),s(declaredFirstByteMs,"declaredFirstByteMs");const secs=s(o=>`${(o/1e3).toFixed(1)}s`,"secs"),upstreams=new WeakMap;function upstreamOpen(o){const c=upstreams.get(o),u=c?.socket;return!!c&&!c.destroyed&&!!u&&!u.destroyed}D(upstreamOpen,"upstreamOpen"),s(upstreamOpen,"upstreamOpen");function wedgedMessage(o,c,u){const q=u.failure==="gone"?"its process is gone":u.failure==="silent"?`it did not answer its liveness check for ${secs(u.silentMs)}`:"it gave no liveness answer";return`Pod ${o} of ${c} sent no response for ${secs(u.heldMs)} and ${q} \u2014 taken out of rotation, and the service is being restarted, so work it was still doing for this request may have been cut short. Retry if the call is safe to repeat.`}D(wedgedMessage,"wedgedMessage"),s(wedgedMessage,"wedgedMessage");function armFirstByteGuard(o){let c,u=!1,q=!1,V=!1,k,x=0,E=(0,body_stall_1.requestBodyArriving)(o.req)?void 0:Date.now();const N=s(()=>{E??=Date.now()},"onBodyEnd");E===void 0&&o.req.once("end",N);const H=s(()=>(E===void 0&&!(0,body_stall_1.requestBodyArriving)(o.req)&&(E=Date.now()),E===void 0?0:Date.now()-E),"heldMs"),A=s(T=>{c=setTimeout(C,T),c.unref?.()},"arm"),C=s(()=>{if(u||!o.pending())return;const T=H();if(o.ceilingMs<=0){if(E!==void 0&&T>=o.firstByteMs){o.onWedged(`no response within ${T}ms (wedged socket) \u2014 evicted + respawning`,{heldMs:T,silentMs:0,probes:0,failure:"deadline"});return}A(E===void 0?o.firstByteMs:o.firstByteMs-T);return}(0,pod_probe_1.probePodAliveVerdict)(o.socketPath,void 0,o.serviceKey).then(({alive:L,failure:b})=>{if(u||!o.pending())return;const j=H();if(!L){if(b==="gone"&&o.upstreamOpen?.()){if(j>=o.ceilingMs){o.onCeiling(`retiring (its socket is gone) and no response within ${j}ms (ceiling ${o.ceilingMs}ms) \u2014 giving up on this request, pod kept`);return}V||(V=!0,o.onRetiring?.(j)),A(Math.max(o.firstByteMs,types_1.POD_ALIVE_PROBE_TIMEOUT_MS));return}if(x+=1,b==="silent"&&(0,gc_notice_1.inAnnouncedGc)(o.socketPath)){k=void 0,A(types_1.POD_ALIVE_PROBE_TIMEOUT_MS);return}if(b==="silent"&&types_1.POD_WEDGE_CONFIRM_MS>0&&(k??=Date.now(),Date.now()-k<types_1.POD_WEDGE_CONFIRM_MS)){A(types_1.POD_ALIVE_PROBE_TIMEOUT_MS);return}const B=k===void 0?0:Date.now()-k,I=b==="gone"?"its socket no longer accepts (the pod process is gone)":b==="silent"?`nothing answered on ${types_1.POD_ALIVE_PATH} for ${B}ms (${x} probe${x===1?"":"s"})`:`no liveness answer on ${types_1.POD_ALIVE_PATH}`;o.onWedged(`no response within ${j}ms and ${I} (wedged socket) \u2014 evicted + respawning`,{heldMs:j,silentMs:B,probes:x,failure:b??"no-route"});return}if(k=void 0,j>=o.ceilingMs){o.onCeiling(`alive on ${types_1.POD_ALIVE_PATH} but no response within ${j}ms (ceiling ${o.ceilingMs}ms) \u2014 giving up on this request, pod kept`,L);return}!q&&E!==void 0&&(q=!0,o.onSlow(j,L)),A(Math.max(o.firstByteMs,types_1.POD_ALIVE_PROBE_TIMEOUT_MS))})},"onDeadline");return A(o.firstByteMs),{clear:s(()=>{u=!0,c&&clearTimeout(c),o.req.removeListener("end",N)},"clear")}}D(armFirstByteGuard,"armFirstByteGuard"),s(armFirstByteGuard,"armFirstByteGuard");function createProxy(o,c,u){const{recordEdge:q,finishTrace:V,traceStep:k,emit:x}=c,E=s((e,r,t,d)=>{const g=d.kind==="pod-evicted";x(g?"error":"warning",`${g?"pod evicted":"request given up"}: ${e.project} (${t}) \u2014 ${d.status}`,`cluster-pod-incident:${e.project}`,{kind:d.kind,project:e.project,pod:t,status:d.status,reason:d.reason,traceId:(0,gateway_errors_1.ensureRequestId)(r),method:r.method,path:(r.url??"").split("?")[0],...d.detail??{}})},"reportPodIncident"),N=s(async(e,r)=>{const t=e.pool;if(!t)return;t.scannedAt=0;let d=(0,discovery_1.pickSock)(t,o.scanTtlMs);return!d&&e.svc&&(k(r,"cold-start",{project:e.project}),await u.ensureUp(e.svc)&&(t.scannedAt=0,d=(0,discovery_1.pickSock)(t,o.scanTtlMs))),d},"anotherPod"),H=s((e,r,t,d,g)=>{console.log(`[gateway] pod ${t} of ${e.project} is gone \u2014 its ${r.method} was not answered, sending it to ${d}`),x("info",`request moved to another pod: ${e.project} (${t} \u2192 ${d})`,`cluster-pod-moved:${e.project}`,{kind:"pod-gone-rerouted",project:e.project,pod:t,to:d,reason:g,traceId:(0,gateway_errors_1.ensureRequestId)(r),method:r.method,path:(r.url??"").split("?")[0]})},"reportPodMoved"),A=s(async e=>{if(!e.lens||!o.lensSupervisor)return"unsupported";let r;const t=new Promise(f=>{r=setTimeout(()=>f("timeout"),types_1.LENS_REQUEST_RECOVERY_WAIT_MS),r.unref?.()}),d=o.lensSupervisor.recover(e.lens,"proxy-connection").then(f=>f?"ready":"failed").catch(()=>"failed"),g=await Promise.race([d,t]);r&&clearTimeout(r);const S=o.lensSupervisor.status(e.lens);return g==="failed"&&(!S||S.state==="ready")?"unsupported":g},"waitForManagedRecovery"),C=s((e,r,t,d,g,S)=>{if(t.headersSent||t.writableEnded||t.destroyed){t.destroyed||t.destroy();return}const f=e.lens?o.lensSupervisor?.status(e.lens):void 0,$=S==="timeout"||f?.state==="starting"||f?.state==="replacing",P=(0,gateway_errors_1.ensureRequestId)(r),l=e.lens?{project:e.project,reason:f?.reason??"connection-failure",generation:f?.generation??0,...$?{retryAfterMs:2e3}:{}}:void 0,i=$?503:502,y=$?"starting":"unavailable";console.error(`[gateway] ${new Date().toISOString()} upstream-error traceId=${P} project=${e.project} target=${String(d)} state=${f?.state??"unmanaged"} generation=${f?.generation??0}: ${g.message}`),x("warning",`upstream unavailable: ${e.project}`,`cluster-upstream:${e.project}`,{kind:"upstream-error",traceId:P,project:e.project,target:String(d),state:f?.state??"unmanaged",generation:f?.generation,error:g.message}),(0,edge_1.sendError)(t,r,i,y,$?`Upstream ${e.project} is restarting.`:`Upstream ${e.project} unavailable.`,l,$?{"retry-after":"2"}:void 0)},"sendTerminalUpstreamError"),T=(0,body_stall_1.createBodyStallGuard)(o.bodyStallMs??types_1.BODY_STALL_MS),L={keepAlive:!0,keepAliveMsecs:3e4,maxSockets:1024,maxFreeSockets:256},b=new http.Agent(L),j=new(require("node:https")).Agent(L),B=s(e=>typeof e=="string"&&e.startsWith("https:")?j:b,"agentForTarget"),I=httpProxy.createProxyServer({xfwd:!1,secure:!1,proxyTimeout:12e4,agent:b}),Y=s((e,r,t)=>{const d=s(()=>{t()||e.destroyed||e.destroy()},"onEdgeClose");r.once("close",d),e.once("close",()=>r.removeListener("close",d))},"propagateEdgeCancel");I.on("proxyReq",(e,r,t)=>{upstreams.set(r,e);for(const d of UPSTREAM_CONNECTION_HEADERS)e.removeHeader(d);e.setHeader("connection","keep-alive"),Y(e,t,()=>t.writableEnded)}),I.on("proxyRes",(e,r,t)=>{const d=r.__lensmcpPod;d&&(e.headers["x-lensmcp-pod"]=d),delete e.headers["x-powered-by"],e.headers.server="LensMCP",returnRequestId(e.headers,r),t&&Y(e,t,()=>t.writableEnded),t&&T.armResponse(t)});const Q=s((e,r,t,d)=>{(0,edge_1.stampForwardingHeaders)(o,r),T.arm(r);const g={},S=r.method==="GET"||r.method==="HEAD"||r.method==="OPTIONS";for(const[n,a]of Object.entries(r.headers))a===void 0||n.startsWith(":")||HOP_BY_HOP.has(n)||S&&(n==="content-length"||n==="transfer-encoding")||(g[n]=a);let f=http,$;if(typeof e=="object")$={socketPath:e.socketPath,path:r.url,method:r.method,headers:g,agent:b};else{const n=new URL(e),a=n.protocol==="https:";f=a?require("node:https"):http,g.host=n.host,$={protocol:n.protocol,hostname:n.hostname,port:n.port||(a?443:80),path:r.url,method:r.method,headers:g,agent:B(e)}}const P=t;P.__lensmcpErrContained||(P.__lensmcpErrContained=!0,t.on("error",()=>{}));let l;const i=s(()=>{t.writableEnded||p.destroy()},"onReqGone"),y=s(()=>{l&&!l.destroyed&&l.destroy()},"onEdgeClose"),_=s(()=>{r.removeListener("error",i),r.removeListener("aborted",i),r.removeListener("close",i),t.removeListener("close",y)},"detach"),p=f.request($,n=>{if(l=n,d.onResponse(),t.headersSent||t.writableEnded||t.destroyed){n.destroy();return}const a={};for(const[O,U]of Object.entries(n.headers))U===void 0||HOP_BY_HOP.has(O)||(a[O]=U);delete a["x-powered-by"],a.server="LensMCP",returnRequestId(a,r);const w=r.__lensmcpPod;w&&(a["x-lensmcp-pod"]=w);try{t.writeHead(n.statusCode??502,a)}catch(O){console.error("[gateway] h2 relay: bad response header, resetting stream:",O.message),n.destroy(),t.destroyed||t.destroy(O);return}n.pipe(t),T.armResponse(t);const R=s(()=>{!t.writableEnded&&!t.destroyed&&t.destroy()},"abortEdge");n.once("error",R),n.once("close",R),t.on("close",y)});p.once("error",n=>d.onError(n)),upstreams.set(r,p),p.once("close",_),r.on("error",i),r.on("aborted",i),r.on("close",i),S?p.end():r.pipe(p)},"pipeUpstream"),X=s(async(e,r,t)=>{const d=r.__lensmcpTrace;if(e.pool){const l=e.svc;l&&(l.lastUsed=Date.now(),l.inflight+=1,t.on("close",()=>{l.inflight-=1,l.lastUsed=Date.now()}));let i=(0,discovery_1.pickSock)(e.pool,o.scanTtlMs);if(!i&&l&&(k(d,"cold-start",{project:e.project}),await u.ensureUp(l)&&(i=(0,discovery_1.pickSock)(e.pool,o.scanTtlMs))),l&&i&&u.markUp(l,"observed"),l&&u.scaleUp(l),!i){const _=l?.lastErrors?.length?` Service errors: ${l.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(t,r,503,"unavailable",`Service ${e.project} is not reachable (no pods in ${e.pool.dir}).${_} The watcher recovers and the next request retries automatically.`);return}const y=s((_,p)=>{const n=path.basename(_,".sock");r.__lensmcpPod=n,k(d,"pod-select",{pod:n,pods:e.pool.socks.length,strategy:"round-robin",...p?{movedFrom:p}:{}});let a=!1;const w=s((h,M="unlink")=>{if(e.pool.socks=e.pool.socks.filter(m=>m!==_),e.pool.idx=-1,M==="unlink")try{fs.unlinkSync(_)}catch{}console.error(`[gateway] pod ${n} of ${e.project}: ${h}`)},"evictPod"),R=armFirstByteGuard({socketPath:_,serviceKey:o.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)],req:r,firstByteMs:declaredFirstByteMs(e.svc?.decl?.firstByteMs),ceilingMs:types_1.POD_SLOW_FIRST_BYTE_MS,pending:s(()=>!a&&!t.headersSent&&!t.writableEnded,"pending"),upstreamOpen:s(()=>upstreamOpen(r),"upstreamOpen"),onRetiring:s(h=>{console.log(`[gateway] pod ${n} of ${e.project}: retiring (its socket is gone) but still holds this ${r.method} after ${h}ms \u2014 waiting for it, not evicting`)},"onRetiring"),onWedged:s((h,M)=>{a=!0,w(h),E(e,r,n,{kind:"pod-evicted",status:502,reason:h,detail:M}),(0,edge_1.sendError)(t,r,502,"unavailable",wedgedMessage(n,e.project,M))},"onWedged"),onCeiling:s(h=>{a=!0,w(h,"keep"),E(e,r,n,{kind:"request-gave-up",status:504,reason:h}),(0,edge_1.sendError)(t,r,504,"unavailable",`Pod ${n} of ${e.project} did not answer in time \u2014 retry, or raise cluster.firstByteMs. The pod was kept: the request may still complete there.`)},"onCeiling"),onSlow:s((h,M)=>{console.log(`[gateway] pod ${n} of ${e.project}: slow first byte, pod alive (${h}ms, ${M.inflight} in-flight, ${M.activeWork} background) \u2014 waiting, not evicting. Set cluster.firstByteMs on ${e.project} to size this route's budget.`)},"onSlow")});t.on("close",()=>R.clear());const O=_,U=isReplayableRequest(r);let W=0;const F=s(()=>{W+=1,Q({socketPath:O},r,t,{onResponse:s(()=>{R.clear(),a=!0},"onResponse"),onError:s(h=>{if(!a){if(edgeGone(r,t)){R.clear(),a=!0;return}(0,pod_probe_1.podConnectionVerdict)(O,h).then(M=>{if(!a){if(M==="alive"&&W<2&&U&&!edgeGone(r,t)){console.log(`[gateway] pod ${n} of ${e.project}: ${h.message} on one connection, the pod still accepts \u2014 sending the ${r.method} again on a fresh one`),F();return}if(R.clear(),a=!0,M==="gone"){w(h.message);const m=s(()=>{E(e,r,n,{kind:"pod-evicted",status:502,reason:`${h.message} \u2014 its socket no longer accepts (the pod process is gone)${p?`; the request had already been moved here from ${p}`:""}`}),(0,edge_1.sendError)(t,r,502,"unavailable",`Pod ${n} of ${e.project} is gone (${h.message}) \u2014 retry.`)},"giveUp");if(p||!U){m();return}N(e,d).then(v=>{if(!edgeGone(r,t)){if(!v){m();return}H(e,r,n,path.basename(v,".sock"),h.message),y(v,n)}},m);return}console.error(`[gateway] pod ${n} of ${e.project}: ${h.message} on one connection \u2014 the pod still accepts connections, so it stays in the pool`),(0,edge_1.sendError)(t,r,502,"unavailable",`Pod ${n} of ${e.project} dropped the connection \u2014 retry.`)}})}},"onError")})},"sendToPod");F()},"onPod");y(i);return}const g=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let S=0,f=!1;const $=isReplayableRequest(r),P=s(()=>{if(edgeGone(r,t))return;const l=e.target;Q(l,r,t,{onResponse:s(()=>{},"onResponse"),onError:s(i=>{if(isConnError(i)&&(S+=1),e.lens&&isConnError(i)&&$&&S>=2&&!f&&!edgeGone(r,t)){f=!0,A(e).then(y=>{if(!edgeGone(r,t)){if(y==="ready"){P();return}if(y==="unsupported"&&Date.now()<g){setTimeout(P,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}C(e,r,t,e.target,i,y)}});return}if(isConnError(i)&&$&&Date.now()<g&&!edgeGone(r,t)){setTimeout(P,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}if(t.headersSent||t.writableEnded||t.destroyed){t.destroyed||t.destroy();return}C(e,r,t,l,i)},"onError")})},"tryTcp");P()},"forwardNative");return{proxy:I,forward:s(async(e,r,t)=>{const d=Date.now(),g=r.__lensmcpTrace;if(t.on("close",()=>{q(e,Date.now()-d,t.statusCode??0,r.__lensmcpCaller),V(g,e.project,t.statusCode??0,{...r.__lensmcpPod?{pod:r.__lensmcpPod}:{},...r.__lensmcpCaller?{caller:r.__lensmcpCaller}:{}},e)}),e.prependPrefix&&!r.url?.startsWith(e.prependPrefix)&&(r.url=e.prependPrefix+(r.url??"/")),(0,edge_1.stampForwardingHeaders)(o,r),T.arm(r),(r.httpVersionMajor??1)>=2){await X(e,r,t);return}if(e.pool){const i=e.svc;i&&(i.lastUsed=Date.now(),i.inflight+=1,t.on("close",()=>{i.inflight-=1,i.lastUsed=Date.now()}));let y=(0,discovery_1.pickSock)(e.pool,o.scanTtlMs);if(!y&&i&&(k(g,"cold-start",{project:e.project}),await u.ensureUp(i)&&(y=(0,discovery_1.pickSock)(e.pool,o.scanTtlMs))),i&&y&&u.markUp(i,"observed"),i&&u.scaleUp(i),!y){const p=i?.lastErrors?.length?` Service errors: ${i.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(t,r,503,"unavailable",`Service ${e.project} is not reachable (no pods in ${e.pool.dir}).${p} The watcher recovers and the next request retries automatically.`);return}const _=s((p,n)=>{const a=path.basename(p,".sock");r.__lensmcpPod=a,k(g,"pod-select",{pod:a,pods:e.pool.socks.length,strategy:"round-robin",...n?{movedFrom:n}:{}});let w=!1;const R=s((m,v="unlink")=>{if(e.pool.socks=e.pool.socks.filter(G=>G!==p),e.pool.idx=-1,v==="unlink")try{fs.unlinkSync(p)}catch{}console.error(`[gateway] pod ${a} of ${e.project}: ${m}`)},"evictPod"),O=armFirstByteGuard({socketPath:p,serviceKey:o.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)],req:r,firstByteMs:declaredFirstByteMs(e.svc?.decl?.firstByteMs),ceilingMs:types_1.POD_SLOW_FIRST_BYTE_MS,pending:s(()=>!w&&!t.headersSent&&!t.writableEnded,"pending"),upstreamOpen:s(()=>upstreamOpen(r),"upstreamOpen"),onRetiring:s(m=>{console.log(`[gateway] pod ${a} of ${e.project}: retiring (its socket is gone) but still holds this ${r.method} after ${m}ms \u2014 waiting for it, not evicting`)},"onRetiring"),onWedged:s((m,v)=>{w=!0,R(m),E(e,r,a,{kind:"pod-evicted",status:502,reason:m,detail:v}),(0,edge_1.sendError)(t,r,502,"unavailable",wedgedMessage(a,e.project,v))},"onWedged"),onCeiling:s(m=>{w=!0,R(m,"keep"),E(e,r,a,{kind:"request-gave-up",status:504,reason:m}),(0,edge_1.sendError)(t,r,504,"unavailable",`Pod ${a} of ${e.project} did not answer in time \u2014 retry, or raise cluster.firstByteMs. The pod was kept: the request may still complete there.`)},"onCeiling"),onSlow:s((m,v)=>{console.log(`[gateway] pod ${a} of ${e.project}: slow first byte, pod alive (${m}ms, ${v.inflight} in-flight, ${v.activeWork} background) \u2014 waiting, not evicting. Set cluster.firstByteMs on ${e.project} to size this route's budget.`)},"onSlow")});t.on("close",()=>O.clear());const U=Math.max(12e4,types_1.POD_SLOW_FIRST_BYTE_MS+6e4),W=p,F=isReplayableRequest(r);let h=0;const M=s(()=>{h+=1;const m=Date.now();I.web(r,t,{target:{socketPath:W},agent:b,proxyTimeout:U},v=>{if(!w){if(edgeGone(r,t)){O.clear(),w=!0;return}(0,pod_probe_1.podConnectionVerdict)(W,v).then(G=>{if(w)return;const J=Date.now()-m>=U-250;if(G==="alive"&&h<2&&F&&!J&&!edgeGone(r,t)){console.log(`[gateway] pod ${a} of ${e.project}: ${v.message} on one connection, the pod still accepts \u2014 sending the ${r.method} again on a fresh one`),M();return}if(O.clear(),w=!0,G==="gone"){R(v.message);const K=s(()=>{E(e,r,a,{kind:"pod-evicted",status:502,reason:`${v.message} \u2014 its socket no longer accepts (the pod process is gone)${n?`; the request had already been moved here from ${n}`:""}`}),(0,edge_1.sendError)(t,r,502,"unavailable",`Pod ${a} of ${e.project} is gone (${v.message}) \u2014 retry.`)},"giveUp");if(n||!F||J){K();return}N(e,g).then(z=>{if(!edgeGone(r,t)){if(!z){K();return}H(e,r,a,path.basename(z,".sock"),v.message),_(z,a)}},K);return}console.error(`[gateway] pod ${a} of ${e.project}: ${v.message} on one connection \u2014 the pod still accepts connections, so it stays in the pool`),(0,edge_1.sendError)(t,r,502,"unavailable",`Pod ${a} of ${e.project} dropped the connection \u2014 retry.`)})}})},"sendToPod");M()},"onPod");_(y);return}const S=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let f=0,$=!1;const P=isReplayableRequest(r),l=s(()=>{if(edgeGone(r,t))return;const i=e.target,y={target:i,autoRewrite:typeof i=="string",changeOrigin:!0,agent:B(i)};I.web(r,t,y,_=>{if(isConnError(_)&&(f+=1),e.lens&&isConnError(_)&&P&&f>=2&&!$&&!edgeGone(r,t)){$=!0,A(e).then(p=>{if(!edgeGone(r,t)){if(p==="ready"){l();return}if(p==="unsupported"&&Date.now()<S){setTimeout(l,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}C(e,r,t,e.target,_,p)}});return}if(isConnError(_)&&P&&Date.now()<S&&!edgeGone(r,t)){setTimeout(l,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}t.destroyed||C(e,r,t,i,_)})},"tryTcp");l()},"forward"),agentForTarget:B,destroyAgents:s(()=>{T.stop(),b.destroy(),j.destroy()},"destroyAgents"),closeProxy:s(()=>{try{I.close?.()}catch{}},"closeProxy")}}D(createProxy,"createProxy"),s(createProxy,"createProxy");
|
|
1
|
+
"use strict";var re=Object.defineProperty;var R=(o,i)=>re(o,"name",{value:i,configurable:!0});var q=Object.defineProperty,s=R((o,i)=>q(o,"name",{value:i,configurable:!0}),"s");Object.defineProperty(exports,"__esModule",{value:!0}),exports.isReplayableRequest=isReplayableRequest,exports.upstreamOpen=upstreamOpen,exports.wedgedMessage=wedgedMessage,exports.createProxy=createProxy;const tslib_1=require("tslib"),fs=tslib_1.__importStar(require("node:fs")),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),edge_1=require("./edge"),gateway_errors_1=require("../gateway-errors"),body_stall_1=require("../body-stall"),types_1=require("./types"),pod_probe_1=require("./pod-probe"),gc_notice_1=require("../../../gc-notice"),service_keys_1=require("./service-keys");function isConnError(o){const i=o.code??"";return i==="ECONNREFUSED"||i==="ECONNRESET"||i==="ETIMEDOUT"||i==="EPIPE"||i==="EHOSTUNREACH"||i==="ENOENT"||/ECONNREFUSED|ECONNRESET|socket hang up|EPIPE/i.test(o.message)}R(isConnError,"isConnError"),s(isConnError,"isConnError");function isDroppedConnection(o){const i=o.code??"";return i==="ECONNRESET"||i==="EPIPE"||/ECONNRESET|socket hang up|EPIPE/i.test(o.message)}R(isDroppedConnection,"isDroppedConnection"),s(isDroppedConnection,"isDroppedConnection");const IDEMPOTENT_METHODS=new Set(["GET","HEAD","OPTIONS","PUT","DELETE","TRACE"]);function isReplayableRequest(o){if(!IDEMPOTENT_METHODS.has((o.method??"GET").toUpperCase())||o.headers["transfer-encoding"]!==void 0)return!1;const i=o.headers["content-length"];return!(i!==void 0&&String(i).trim()!=="0")}R(isReplayableRequest,"isReplayableRequest"),s(isReplayableRequest,"isReplayableRequest");function edgeGone(o,i){return i.headersSent||i.writableEnded||i.destroyed===!0||o.aborted===!0}R(edgeGone,"edgeGone"),s(edgeGone,"edgeGone");const HOP_BY_HOP=new Set(["connection","keep-alive","proxy-connection","proxy-authenticate","proxy-authorization","te","trailer","transfer-encoding","upgrade"]),UPSTREAM_CONNECTION_HEADERS=["connection","keep-alive","proxy-connection"];function returnRequestId(o,i){if(o["x-request-id"]!==void 0)return;const g=i.headers["x-request-id"],x=Array.isArray(g)?g[0]:g;typeof x=="string"&&x!==""&&(o["x-request-id"]=x)}R(returnRequestId,"returnRequestId"),s(returnRequestId,"returnRequestId");const httpProxy=require("http-proxy");function declaredFirstByteMs(o){const i=Number(o);return Number.isFinite(i)&&i>0?i:types_1.POD_RESPONSE_TIMEOUT_MS}R(declaredFirstByteMs,"declaredFirstByteMs"),s(declaredFirstByteMs,"declaredFirstByteMs");const secs=s(o=>`${(o/1e3).toFixed(1)}s`,"secs"),upstreams=new WeakMap;function upstreamOpen(o){const i=upstreams.get(o),g=i?.socket;return!!i&&!i.destroyed&&!!g&&!g.destroyed}R(upstreamOpen,"upstreamOpen"),s(upstreamOpen,"upstreamOpen");function wedgedMessage(o,i,g){const x=g.failure==="gone"?"its process is gone":g.failure==="silent"?`it did not answer its liveness check for ${secs(g.silentMs)}`:"it gave no liveness answer";return`Pod ${o} of ${i} sent no response for ${secs(g.heldMs)} and ${x} \u2014 taken out of rotation, and the service is being restarted, so work it was still doing for this request may have been cut short. Retry if the call is safe to repeat.`}R(wedgedMessage,"wedgedMessage"),s(wedgedMessage,"wedgedMessage");function armFirstByteGuard(o){let i,g=!1,x=!1,K=!1,k,C=0,_=(0,body_stall_1.requestBodyArriving)(o.req)?void 0:Date.now();const H=s(()=>{_??=Date.now()},"onBodyEnd");_===void 0&&o.req.once("end",H);const W=s(()=>(_===void 0&&!(0,body_stall_1.requestBodyArriving)(o.req)&&(_=Date.now()),_===void 0?0:Date.now()-_),"heldMs"),I=s(b=>{i=setTimeout(L,b),i.unref?.()},"arm"),L=s(()=>{if(g||!o.pending())return;const b=W();if(o.ceilingMs<=0){if(_!==void 0&&b>=o.firstByteMs){o.onWedged(`no response within ${b}ms (wedged socket) \u2014 evicted + respawning`,{heldMs:b,silentMs:0,probes:0,failure:"deadline"});return}I(_===void 0?o.firstByteMs:o.firstByteMs-b);return}(0,pod_probe_1.probePodAliveVerdict)(o.socketPath,void 0,o.serviceKey).then(({alive:B,failure:j})=>{if(g||!o.pending())return;const D=W();if(!B){if(j==="gone"&&o.upstreamOpen?.()){if(D>=o.ceilingMs){o.onCeiling(`retiring (its socket is gone) and no response within ${D}ms (ceiling ${o.ceilingMs}ms) \u2014 giving up on this request, pod kept`);return}K||(K=!0,o.onRetiring?.(D)),I(Math.max(o.firstByteMs,types_1.POD_ALIVE_PROBE_TIMEOUT_MS));return}if(C+=1,j==="silent"&&(0,gc_notice_1.inAnnouncedGc)(o.socketPath)){k=void 0,I(types_1.POD_ALIVE_PROBE_TIMEOUT_MS);return}if(j==="silent"&&types_1.POD_WEDGE_CONFIRM_MS>0&&(k??=Date.now(),Date.now()-k<types_1.POD_WEDGE_CONFIRM_MS)){I(types_1.POD_ALIVE_PROBE_TIMEOUT_MS);return}const F=k===void 0?0:Date.now()-k,U=j==="gone"?"its socket no longer accepts (the pod process is gone)":j==="silent"?`nothing answered on ${types_1.POD_ALIVE_PATH} for ${F}ms (${C} probe${C===1?"":"s"})`:`no liveness answer on ${types_1.POD_ALIVE_PATH}`;o.onWedged(`no response within ${D}ms and ${U} (wedged socket) \u2014 evicted + respawning`,{heldMs:D,silentMs:F,probes:C,failure:j??"no-route"});return}if(k=void 0,D>=o.ceilingMs){o.onCeiling(`alive on ${types_1.POD_ALIVE_PATH} but no response within ${D}ms (ceiling ${o.ceilingMs}ms) \u2014 giving up on this request, pod kept`,B);return}!x&&_!==void 0&&(x=!0,o.onSlow(D,B)),I(Math.max(o.firstByteMs,types_1.POD_ALIVE_PROBE_TIMEOUT_MS))})},"onDeadline");return I(o.firstByteMs),{clear:s(()=>{g=!0,i&&clearTimeout(i),o.req.removeListener("end",H)},"clear")}}R(armFirstByteGuard,"armFirstByteGuard"),s(armFirstByteGuard,"armFirstByteGuard");function createProxy(o,i,g){const{recordEdge:x,finishTrace:K,traceStep:k,emit:C}=i,_=s((e,r,t,c)=>{const h=c.kind==="pod-evicted";C(h?"error":"warning",`${h?"pod evicted":"request given up"}: ${e.project} (${t}) \u2014 ${c.status}`,`cluster-pod-incident:${e.project}`,{kind:c.kind,project:e.project,pod:t,status:c.status,reason:c.reason,traceId:(0,gateway_errors_1.ensureRequestId)(r),method:r.method,path:(r.url??"").split("?")[0],...c.detail??{}})},"reportPodIncident"),H=s(async(e,r)=>{const t=e.pool;if(!t)return;t.scannedAt=0;let c=(0,discovery_1.pickSock)(t,o.scanTtlMs);return!c&&e.svc&&(k(r,"cold-start",{project:e.project}),await g.ensureUp(e.svc)&&(t.scannedAt=0,c=(0,discovery_1.pickSock)(t,o.scanTtlMs))),c},"anotherPod"),W=s((e,r,t,c,h)=>{console.log(`[gateway] pod ${t} of ${e.project} is gone \u2014 its ${r.method} was not answered, sending it to ${c}`),C("info",`request moved to another pod: ${e.project} (${t} \u2192 ${c})`,`cluster-pod-moved:${e.project}`,{kind:"pod-gone-rerouted",project:e.project,pod:t,to:c,reason:h,traceId:(0,gateway_errors_1.ensureRequestId)(r),method:r.method,path:(r.url??"").split("?")[0]})},"reportPodMoved"),I=s(async e=>{if(!e.lens||!o.lensSupervisor)return"unsupported";let r;const t=new Promise(y=>{r=setTimeout(()=>y("timeout"),types_1.LENS_REQUEST_RECOVERY_WAIT_MS),r.unref?.()}),c=o.lensSupervisor.recover(e.lens,"proxy-connection").then(y=>y?"ready":"failed").catch(()=>"failed"),h=await Promise.race([c,t]);r&&clearTimeout(r);const $=o.lensSupervisor.status(e.lens);return h==="failed"&&(!$||$.state==="ready")?"unsupported":h},"waitForManagedRecovery"),L=s((e,r,t,c,h,$)=>{if(t.headersSent||t.writableEnded||t.destroyed){t.destroyed||t.destroy();return}const y=e.lens?o.lensSupervisor?.status(e.lens):void 0,w=$==="timeout"||y?.state==="starting"||y?.state==="replacing",P=(0,gateway_errors_1.ensureRequestId)(r),M=e.lens?{project:e.project,reason:y?.reason??"connection-failure",generation:y?.generation??0,...w?{retryAfterMs:2e3}:{}}:void 0,l=w?503:502,d=w?"starting":"unavailable";console.error(`[gateway] ${new Date().toISOString()} upstream-error traceId=${P} project=${e.project} target=${String(c)} state=${y?.state??"unmanaged"} generation=${y?.generation??0}: ${h.message}`),C("warning",`upstream unavailable: ${e.project}`,`cluster-upstream:${e.project}`,{kind:"upstream-error",traceId:P,project:e.project,target:String(c),state:y?.state??"unmanaged",generation:y?.generation,error:h.message}),(0,edge_1.sendError)(t,r,l,d,w?`Upstream ${e.project} is restarting.`:`Upstream ${e.project} unavailable.`,M,w?{"retry-after":"2"}:void 0)},"sendTerminalUpstreamError"),b=(0,body_stall_1.createBodyStallGuard)(o.bodyStallMs??types_1.BODY_STALL_MS),B={keepAlive:!0,keepAliveMsecs:3e4,maxSockets:1024,maxFreeSockets:256},j=new http.Agent(B),D=new(require("node:https")).Agent(B),F=s(e=>typeof e=="string"&&e.startsWith("https:")?D:j,"agentForTarget"),U=httpProxy.createProxyServer({xfwd:!1,secure:!1,proxyTimeout:12e4,agent:j}),J=s((e,r,t)=>{const c=s(()=>{t()||e.destroyed||e.destroy()},"onEdgeClose");r.once("close",c),e.once("close",()=>r.removeListener("close",c))},"propagateEdgeCancel");U.on("proxyReq",(e,r,t)=>{upstreams.set(r,e);for(const h of UPSTREAM_CONNECTION_HEADERS)e.removeHeader(h);const c=e.agent?.keepAlive!==!1;e.setHeader("connection",c?"keep-alive":"close"),J(e,t,()=>t.writableEnded)}),U.on("proxyRes",(e,r,t)=>{const c=r.__lensmcpPod;c&&(e.headers["x-lensmcp-pod"]=c),delete e.headers["x-powered-by"],e.headers.server="LensMCP",returnRequestId(e.headers,r),t&&J(e,t,()=>t.writableEnded),t&&b.armResponse(t)});const X=s((e,r,t,c,h=!1)=>{(0,edge_1.stampForwardingHeaders)(o,r),b.arm(r);const $={},y=r.method==="GET"||r.method==="HEAD"||r.method==="OPTIONS";for(const[n,a]of Object.entries(r.headers))a===void 0||n.startsWith(":")||HOP_BY_HOP.has(n)||y&&(n==="content-length"||n==="transfer-encoding")||($[n]=a);let w=http,P;if(typeof e=="object")P={socketPath:e.socketPath,path:r.url,method:r.method,headers:$,agent:j};else{const n=new URL(e),a=n.protocol==="https:";w=a?require("node:https"):http,$.host=n.host,P={protocol:n.protocol,hostname:n.hostname,port:n.port||(a?443:80),path:r.url,method:r.method,headers:$,agent:h?!1:F(e)}}const M=t;M.__lensmcpErrContained||(M.__lensmcpErrContained=!0,t.on("error",()=>{}));let l;const d=s(()=>{t.writableEnded||p.destroy()},"onReqGone"),u=s(()=>{l&&!l.destroyed&&l.destroy()},"onEdgeClose"),m=s(()=>{r.removeListener("error",d),r.removeListener("aborted",d),r.removeListener("close",d),t.removeListener("close",u)},"detach"),p=w.request(P,n=>{if(l=n,c.onResponse(),t.headersSent||t.writableEnded||t.destroyed){n.destroy();return}const a={};for(const[O,N]of Object.entries(n.headers))N===void 0||HOP_BY_HOP.has(O)||(a[O]=N);delete a["x-powered-by"],a.server="LensMCP",returnRequestId(a,r);const S=r.__lensmcpPod;S&&(a["x-lensmcp-pod"]=S);try{t.writeHead(n.statusCode??502,a)}catch(O){console.error("[gateway] h2 relay: bad response header, resetting stream:",O.message),n.destroy(),t.destroyed||t.destroy(O);return}n.pipe(t),b.armResponse(t);const A=s(()=>{!t.writableEnded&&!t.destroyed&&t.destroy()},"abortEdge");n.once("error",A),n.once("close",A),t.on("close",u)});p.once("error",n=>c.onError(n)),upstreams.set(r,p),p.once("close",m),r.on("error",d),r.on("aborted",d),r.on("close",d),y?p.end():r.pipe(p)},"pipeUpstream"),ee=s(async(e,r,t)=>{const c=r.__lensmcpTrace;if(e.pool){const l=e.svc;l&&(l.lastUsed=Date.now(),l.inflight+=1,t.on("close",()=>{l.inflight-=1,l.lastUsed=Date.now()}));let d=(0,discovery_1.pickSock)(e.pool,o.scanTtlMs);if(!d&&l&&(k(c,"cold-start",{project:e.project}),await g.ensureUp(l)&&(d=(0,discovery_1.pickSock)(e.pool,o.scanTtlMs))),l&&d&&g.markUp(l,"observed"),l&&g.scaleUp(l),!d){const m=l?.lastErrors?.length?` Service errors: ${l.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(t,r,503,"unavailable",`Service ${e.project} is not reachable (no pods in ${e.pool.dir}).${m} The watcher recovers and the next request retries automatically.`);return}const u=s((m,p)=>{const n=path.basename(m,".sock");r.__lensmcpPod=n,k(c,"pod-select",{pod:n,pods:e.pool.socks.length,strategy:"round-robin",...p?{movedFrom:p}:{}});let a=!1;const S=s((f,T="unlink")=>{if(e.pool.socks=e.pool.socks.filter(v=>v!==m),e.pool.idx=-1,T==="unlink")try{fs.unlinkSync(m)}catch{}console.error(`[gateway] pod ${n} of ${e.project}: ${f}`)},"evictPod"),A=armFirstByteGuard({socketPath:m,serviceKey:o.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)],req:r,firstByteMs:declaredFirstByteMs(e.svc?.decl?.firstByteMs),ceilingMs:types_1.POD_SLOW_FIRST_BYTE_MS,pending:s(()=>!a&&!t.headersSent&&!t.writableEnded,"pending"),upstreamOpen:s(()=>upstreamOpen(r),"upstreamOpen"),onRetiring:s(f=>{console.log(`[gateway] pod ${n} of ${e.project}: retiring (its socket is gone) but still holds this ${r.method} after ${f}ms \u2014 waiting for it, not evicting`)},"onRetiring"),onWedged:s((f,T)=>{a=!0,S(f),_(e,r,n,{kind:"pod-evicted",status:502,reason:f,detail:T}),(0,edge_1.sendError)(t,r,502,"unavailable",wedgedMessage(n,e.project,T))},"onWedged"),onCeiling:s(f=>{a=!0,S(f,"keep"),_(e,r,n,{kind:"request-gave-up",status:504,reason:f}),(0,edge_1.sendError)(t,r,504,"unavailable",`Pod ${n} of ${e.project} did not answer in time \u2014 retry, or raise cluster.firstByteMs. The pod was kept: the request may still complete there.`)},"onCeiling"),onSlow:s((f,T)=>{console.log(`[gateway] pod ${n} of ${e.project}: slow first byte, pod alive (${f}ms, ${T.inflight} in-flight, ${T.activeWork} background) \u2014 waiting, not evicting. Set cluster.firstByteMs on ${e.project} to size this route's budget.`)},"onSlow")});t.on("close",()=>A.clear());const O=m,N=isReplayableRequest(r);let G=0;const V=s(()=>{G+=1,X({socketPath:O},r,t,{onResponse:s(()=>{A.clear(),a=!0},"onResponse"),onError:s(f=>{if(!a){if(edgeGone(r,t)){A.clear(),a=!0;return}(0,pod_probe_1.podConnectionVerdict)(O,f).then(T=>{if(!a){if(T==="alive"&&G<2&&N&&!edgeGone(r,t)){console.log(`[gateway] pod ${n} of ${e.project}: ${f.message} on one connection, the pod still accepts \u2014 sending the ${r.method} again on a fresh one`),V();return}if(A.clear(),a=!0,T==="gone"){S(f.message);const v=s(()=>{_(e,r,n,{kind:"pod-evicted",status:502,reason:`${f.message} \u2014 its socket no longer accepts (the pod process is gone)${p?`; the request had already been moved here from ${p}`:""}`}),(0,edge_1.sendError)(t,r,502,"unavailable",`Pod ${n} of ${e.project} is gone (${f.message}) \u2014 retry.`)},"giveUp");if(p||!N){v();return}H(e,c).then(E=>{if(!edgeGone(r,t)){if(!E){v();return}W(e,r,n,path.basename(E,".sock"),f.message),u(E,n)}},v);return}console.error(`[gateway] pod ${n} of ${e.project}: ${f.message} on one connection \u2014 the pod still accepts connections, so it stays in the pool`),(0,edge_1.sendError)(t,r,502,"unavailable",`Pod ${n} of ${e.project} dropped the connection \u2014 retry.`)}})}},"onError")})},"sendToPod");V()},"onPod");u(d);return}const h=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let $=0,y=!1;const w=isReplayableRequest(r);let P=!1;const M=s((l=!1)=>{if(edgeGone(r,t))return;const d=e.target;X(d,r,t,{onResponse:s(()=>{},"onResponse"),onError:s(u=>{if(isConnError(u)&&($+=1),e.lens&&isConnError(u)&&w&&$>=2&&!y&&!edgeGone(r,t)){y=!0,I(e).then(m=>{if(!edgeGone(r,t)){if(m==="ready"){M();return}if(m==="unsupported"&&Date.now()<h){setTimeout(M,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}L(e,r,t,e.target,u,m)}});return}if(isConnError(u)&&w&&Date.now()<h&&!edgeGone(r,t)){setTimeout(M,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}if(isDroppedConnection(u)&&w&&!P&&!t.headersSent&&!edgeGone(r,t)){P=!0,console.log(`[gateway] ${e.project}: ${u.message} after the heal window \u2014 sending the ${r.method} again on a fresh connection`),M(!0);return}if(t.headersSent||t.writableEnded||t.destroyed){t.destroyed||t.destroy();return}L(e,r,t,d,u)},"onError")},l)},"tryTcp");M()},"forwardNative");return{proxy:U,forward:s(async(e,r,t)=>{const c=Date.now(),h=r.__lensmcpTrace;if(t.on("close",()=>{const d=t.headersSent,u=d?t.statusCode??0:void 0,m=d?t.writableFinished?{}:{aborted:"during-response",error:"the caller went away before the response ended"}:{aborted:"before-response",error:"the caller went away before any response was sent"};x(e,Date.now()-c,u,r.__lensmcpCaller),K(h,e.project,u,{...m,...r.__lensmcpPod?{pod:r.__lensmcpPod}:{},...r.__lensmcpCaller?{caller:r.__lensmcpCaller}:{}},e)}),e.prependPrefix&&!r.url?.startsWith(e.prependPrefix)&&(r.url=e.prependPrefix+(r.url??"/")),(0,edge_1.stampForwardingHeaders)(o,r),b.arm(r),(r.httpVersionMajor??1)>=2){await ee(e,r,t);return}if(e.pool){const d=e.svc;d&&(d.lastUsed=Date.now(),d.inflight+=1,t.on("close",()=>{d.inflight-=1,d.lastUsed=Date.now()}));let u=(0,discovery_1.pickSock)(e.pool,o.scanTtlMs);if(!u&&d&&(k(h,"cold-start",{project:e.project}),await g.ensureUp(d)&&(u=(0,discovery_1.pickSock)(e.pool,o.scanTtlMs))),d&&u&&g.markUp(d,"observed"),d&&g.scaleUp(d),!u){const p=d?.lastErrors?.length?` Service errors: ${d.lastErrors.join("; ")}`:"";(0,edge_1.sendError)(t,r,503,"unavailable",`Service ${e.project} is not reachable (no pods in ${e.pool.dir}).${p} The watcher recovers and the next request retries automatically.`);return}const m=s((p,n)=>{const a=path.basename(p,".sock");r.__lensmcpPod=a,k(h,"pod-select",{pod:a,pods:e.pool.socks.length,strategy:"round-robin",...n?{movedFrom:n}:{}});let S=!1;const A=s((v,E="unlink")=>{if(e.pool.socks=e.pool.socks.filter(Y=>Y!==p),e.pool.idx=-1,E==="unlink")try{fs.unlinkSync(p)}catch{}console.error(`[gateway] pod ${a} of ${e.project}: ${v}`)},"evictPod"),O=armFirstByteGuard({socketPath:p,serviceKey:o.serviceKeys[(0,service_keys_1.serviceKeyId)(e.svc?.wsKey,e.project)],req:r,firstByteMs:declaredFirstByteMs(e.svc?.decl?.firstByteMs),ceilingMs:types_1.POD_SLOW_FIRST_BYTE_MS,pending:s(()=>!S&&!t.headersSent&&!t.writableEnded,"pending"),upstreamOpen:s(()=>upstreamOpen(r),"upstreamOpen"),onRetiring:s(v=>{console.log(`[gateway] pod ${a} of ${e.project}: retiring (its socket is gone) but still holds this ${r.method} after ${v}ms \u2014 waiting for it, not evicting`)},"onRetiring"),onWedged:s((v,E)=>{S=!0,A(v),_(e,r,a,{kind:"pod-evicted",status:502,reason:v,detail:E}),(0,edge_1.sendError)(t,r,502,"unavailable",wedgedMessage(a,e.project,E))},"onWedged"),onCeiling:s(v=>{S=!0,A(v,"keep"),_(e,r,a,{kind:"request-gave-up",status:504,reason:v}),(0,edge_1.sendError)(t,r,504,"unavailable",`Pod ${a} of ${e.project} did not answer in time \u2014 retry, or raise cluster.firstByteMs. The pod was kept: the request may still complete there.`)},"onCeiling"),onSlow:s((v,E)=>{console.log(`[gateway] pod ${a} of ${e.project}: slow first byte, pod alive (${v}ms, ${E.inflight} in-flight, ${E.activeWork} background) \u2014 waiting, not evicting. Set cluster.firstByteMs on ${e.project} to size this route's budget.`)},"onSlow")});t.on("close",()=>O.clear());const N=Math.max(12e4,types_1.POD_SLOW_FIRST_BYTE_MS+6e4),G=p,V=isReplayableRequest(r);let f=0;const T=s(()=>{f+=1;const v=Date.now();U.web(r,t,{target:{socketPath:G},agent:j,proxyTimeout:N},E=>{if(!S){if(edgeGone(r,t)){O.clear(),S=!0;return}(0,pod_probe_1.podConnectionVerdict)(G,E).then(Y=>{if(S)return;const Z=Date.now()-v>=N-250;if(Y==="alive"&&f<2&&V&&!Z&&!edgeGone(r,t)){console.log(`[gateway] pod ${a} of ${e.project}: ${E.message} on one connection, the pod still accepts \u2014 sending the ${r.method} again on a fresh one`),T();return}if(O.clear(),S=!0,Y==="gone"){A(E.message);const Q=s(()=>{_(e,r,a,{kind:"pod-evicted",status:502,reason:`${E.message} \u2014 its socket no longer accepts (the pod process is gone)${n?`; the request had already been moved here from ${n}`:""}`}),(0,edge_1.sendError)(t,r,502,"unavailable",`Pod ${a} of ${e.project} is gone (${E.message}) \u2014 retry.`)},"giveUp");if(n||!V||Z){Q();return}H(e,h).then(z=>{if(!edgeGone(r,t)){if(!z){Q();return}W(e,r,a,path.basename(z,".sock"),E.message),m(z,a)}},Q);return}console.error(`[gateway] pod ${a} of ${e.project}: ${E.message} on one connection \u2014 the pod still accepts connections, so it stays in the pool`),(0,edge_1.sendError)(t,r,502,"unavailable",`Pod ${a} of ${e.project} dropped the connection \u2014 retry.`)})}})},"sendToPod");T()},"onPod");m(u);return}const $=Date.now()+types_1.UPSTREAM_HEAL_WINDOW_MS;let y=0,w=!1;const P=isReplayableRequest(r);let M=!1;const l=s((d=!1)=>{if(edgeGone(r,t))return;const u=e.target,m={target:u,autoRewrite:typeof u=="string",changeOrigin:!0,agent:d?!1:F(u)};U.web(r,t,m,p=>{if(isConnError(p)&&(y+=1),e.lens&&isConnError(p)&&P&&y>=2&&!w&&!edgeGone(r,t)){w=!0,I(e).then(n=>{if(!edgeGone(r,t)){if(n==="ready"){l();return}if(n==="unsupported"&&Date.now()<$){setTimeout(l,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}L(e,r,t,e.target,p,n)}});return}if(isConnError(p)&&P&&Date.now()<$&&!edgeGone(r,t)){setTimeout(l,types_1.UPSTREAM_HEAL_STEP_MS).unref?.();return}if(isDroppedConnection(p)&&P&&!M&&!t.headersSent&&!edgeGone(r,t)){M=!0,console.log(`[gateway] ${e.project}: ${p.message} after the heal window \u2014 sending the ${r.method} again on a fresh connection`),l(!0);return}t.destroyed||L(e,r,t,u,p)})},"tryTcp");l()},"forward"),agentForTarget:F,destroyAgents:s(()=>{b.stop(),j.destroy(),D.destroy()},"destroyAgents"),closeProxy:s(()=>{try{U.close?.()}catch{}},"closeProxy")}}R(createProxy,"createProxy"),s(createProxy,"createProxy");
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
"use strict";var fe=Object.defineProperty;var T=(r,t)=>fe(r,"name",{value:t,configurable:!0});var ye=Object.defineProperty,o=T((r,t)=>ye(r,"name",{value:t,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0}),exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT=exports.GATEWAY_MAX_H2_STREAMS_DEFAULT=exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT=exports.GATEWAY_MAX_CONNECTIONS_DEFAULT=exports.BENIGN_SOCKET_ERRNOS=void 0,exports.buildEdgeVerifierPool=buildEdgeVerifierPool,exports.edgeKeyWait=edgeKeyWait,exports.withBuiltinHooks=withBuiltinHooks,exports.classifyClientError=classifyClientError,exports.createClientErrorLogGate=createClientErrorLogGate,exports.retirePreviousWorkspace=retirePreviousWorkspace,exports.resolveMaxConnections=resolveMaxConnections,exports.h2BudgetFromEnv=h2BudgetFromEnv,exports.createH2Governor=createH2Governor,exports.startGateway=startGateway;const tslib_1=require("tslib"),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),scope_1=require("./scope"),route_registry_1=require("./route-registry"),act_1=require("./act"),control_1=require("./control"),workspace_middleware_1=require("./workspace-middleware"),workspace_registry_1=require("./workspace-registry"),service_keys_1=require("./service-keys"),source_poller_1=require("./source-poller"),client_ip_1=require("../client-ip"),workspace_log_1=require("./workspace-log"),own_log_1=require("./own-log"),observability_1=require("./observability"),lifecycle_1=require("./lifecycle"),process_names_1=require("./process-names"),project_graph_store_1=require("./project-graph-store"),serve_launch_1=require("./serve-launch"),process_name_1=require("../../../process-name"),protocol_1=require("../../../shared-build/protocol"),lens_children_1=require("./lens-children"),proxy_1=require("./proxy"),auth_1=require("./auth"),hooks_1=require("./hooks"),handler_1=require("./handler"),upgrade_1=require("./upgrade"),jwks_verify_1=require("../jwks-verify"),manifest_1=require("../manifest"),edge_1=require("./edge"),warm_start_1=require("./warm-start"),types_1=require("./types");function insecureLocalFetch(r,t){return new Promise((i,c)=>{const a=(r.startsWith("https:")?require("node:https"):require("node:http")).request(r,{method:"GET",headers:t?.headers,rejectUnauthorized:!1},_=>{const y=[];_.on("data",h=>y.push(h)),_.on("error",c),_.on("end",()=>{const h=_.statusCode??0,v=Buffer.concat(y).toString("utf8");i({ok:h>=200&&h<300,status:h,json:o(async()=>JSON.parse(v),"json")})})});a.on("error",c),t?.signal?.addEventListener("abort",()=>a.destroy(new Error(`JWKS ${r} \u2192 aborted`)),{once:!0}),a.end()})}T(insecureLocalFetch,"insecureLocalFetch"),o(insecureLocalFetch,"insecureLocalFetch");function buildEdgeVerifierPool(r,t={}){const i=process.env.LENSMCP_GW_JWKS_URL,c=process.env.LENSMCP_GW_JWT_ISS,a=new Map;let _,y=!1;const h=o((s,l)=>{console.log(`[gateway] edge JWT: verifying via JWKS ${s}${l}`);const d=(0,jwks_verify_1.createJwksVerifier)({jwksUrl:s,...c?{issuer:c}:{},fetchImpl:t.fetchImpl??insecureLocalFetch});let E=!1;return d.warm({...t.warm,onError:o((M,x,b)=>{E=!0,x===1&&b!==void 0&&console.warn(`[gateway] edge JWKS ${s} not loaded yet (${M.message}) \u2014 retrying in the background`),b===void 0&&console.warn(`[gateway] edge JWKS ${s} still not loaded after ${x} attempts (${M.message}) \u2014 the first token that needs it fetches it`)},"onError")}).then(M=>{M&&E&&console.log(`[gateway] edge JWKS ${s} loaded`)}),d},"make"),v=o(s=>{let l=a.get(s);return l||(l=h(`https://auth.${s}/jwks.json`,` (auto-derived for apex ${s})`),a.set(s,l)),l},"forApex");return{for(s){if(i)return _??=h(i,"");const l=s.split(":")[0].toLowerCase();let d;for(const E of idpApexes(r()))(l===E||l.endsWith(`.${E}`))&&(!d||E.length>d.length)&&(d=E);return d?v(d):void 0},warm(){if(!y){if(i){_??=h(i,"");return}for(const s of idpApexes(r()))v(s)}},stop(){y=!0,_?.stop();for(const s of a.values())s.stop()}}}T(buildEdgeVerifierPool,"buildEdgeVerifierPool"),o(buildEdgeVerifierPool,"buildEdgeVerifierPool");function idpApexes(r){const t=new Set;for(const i of r){const c=i.host;typeof c!="string"||!/^auth\./.test(c)||c.startsWith("internal.")||t.add(c.slice(5))}return t}T(idpApexes,"idpApexes"),o(idpApexes,"idpApexes");function edgeKeyWait(r){return async t=>{const i=t.route;if(!i?.auth||i.internal)return;const{req:c}=t,a=Array.isArray(c.headers.authorization)?c.headers.authorization[0]:c.headers.authorization,_=String(a??"");if(!_.startsWith("Bearer "))return;const y=c.url??"/";if((0,manifest_1.authRuleFor)(i.auth,(0,edge_1.canonicalPath)(y)??y.split("?")[0],(c.method??"GET").toUpperCase()).mode!=="jwt")return;const h=String(c.headers.host??"").split(":")[0].toLowerCase();await r.for(h)?.awaitKey(_.slice(7))}}T(edgeKeyWait,"edgeKeyWait"),o(edgeKeyWait,"edgeKeyWait");function withBuiltinHooks(r,t){return{...r,beforeAuth:[...r.beforeAuth??[],t.keyWait],afterAuth:[t.middleware,...r.afterAuth??[]]}}T(withBuiltinHooks,"withBuiltinHooks"),o(withBuiltinHooks,"withBuiltinHooks"),exports.BENIGN_SOCKET_ERRNOS=new Set(["ECONNRESET","EPIPE","ETIMEDOUT","ECONNABORTED","ECANCELED"]);function classifyClientError(r,t){return r&&exports.BENIGN_SOCKET_ERRNOS.has(r)?{status:null,report:!1,cause:"client-abort"}:r==="HPE_INVALID_EOF_STATE"?{status:null,report:!1,cause:"truncated-at-eof"}:r==="ERR_HTTP_REQUEST_TIMEOUT"?{status:408,report:!0,cause:"request-timeout"}:r==="HPE_HEADER_OVERFLOW"?{status:431,report:!0,cause:"header-overflow"}:r?.startsWith("HPE_")?{status:400,report:!0,cause:"parse-error"}:{status:400,report:!0,cause:r??t??"unknown"}}T(classifyClientError,"classifyClientError"),o(classifyClientError,"classifyClientError");const CLIENT_ERROR_REASON={400:"Bad Request",408:"Request Timeout",431:"Request Header Fields Too Large"};function createClientErrorLogGate(r=1e4){const t=new Map;return i=>{const c=Date.now(),a=t.get(i)??0;return c-a<r?!1:(t.set(i,c),!0)}}T(createClientErrorLogGate,"createClientErrorLogGate"),o(createClientErrorLogGate,"createClientErrorLogGate");function guardInboundSocket(r){r.on("error",t=>{exports.BENIGN_SOCKET_ERRNOS.has(t.code??"")||console.warn(`[gateway] inbound socket error (${t.code??t.message}) \u2014 dropping this connection only`),r.destroy()})}T(guardInboundSocket,"guardInboundSocket"),o(guardInboundSocket,"guardInboundSocket");function retirePreviousWorkspace(r,t,i){const c=r.get(t);if(!c)return!1;r.delete(t),i==="re-registered"&&console.log(`[gateway] workspace '${t}' re-registered \u2014 retiring the children of its previous registration first.`);try{c()}catch(a){console.warn(`[gateway] could not fully retire the previous registration of '${t}' (${i}): ${a.message}`)}return!0}T(retirePreviousWorkspace,"retirePreviousWorkspace"),o(retirePreviousWorkspace,"retirePreviousWorkspace"),exports.GATEWAY_MAX_CONNECTIONS_DEFAULT=512,exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT=64,exports.GATEWAY_MAX_H2_STREAMS_DEFAULT=4096,exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT=3e5;function parseBudget(r,t){if(r===void 0||r.trim()==="")return t;const i=Number(r);return!Number.isFinite(i)||i<0?t:Math.floor(i)}T(parseBudget,"parseBudget"),o(parseBudget,"parseBudget");function resolveMaxConnections(r){return parseBudget(r,exports.GATEWAY_MAX_CONNECTIONS_DEFAULT)}T(resolveMaxConnections,"resolveMaxConnections"),o(resolveMaxConnections,"resolveMaxConnections");function h2BudgetFromEnv(r=process.env){return{maxSessions:parseBudget(r.LENSMCP_MAX_H2_SESSIONS,exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT),maxStreams:parseBudget(r.LENSMCP_MAX_H2_STREAMS,exports.GATEWAY_MAX_H2_STREAMS_DEFAULT),idleSessionMs:parseBudget(r.LENSMCP_H2_IDLE_MS,exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT)}}T(h2BudgetFromEnv,"h2BudgetFromEnv"),o(h2BudgetFromEnv,"h2BudgetFromEnv");function createH2Governor(r,t){const{NGHTTP2_ENHANCE_YOUR_CALM:i,NGHTTP2_REFUSED_STREAM:c}=require("node:http2").constants,a=new Map;let _=0;const y=o(s=>{s.idle&&(clearTimeout(s.idle),s.idle=void 0)},"disarm"),h=o((s,l)=>{y(l),!(r.idleSessionMs<=0||l.open>0)&&(l.idle=setTimeout(()=>{s.destroyed||s.closed||(t("session-reclaimed",{idleMs:r.idleSessionMs,sessions:a.size}),a.delete(s),s.close())},r.idleSessionMs),l.idle.unref?.())},"arm"),v=o(()=>{for(const[s,l]of a)if(!(l.open>0||s.destroyed))return y(l),a.delete(s),t("session-reclaimed",{reason:"cap",sessions:a.size+1,maxSessions:r.maxSessions}),s.close(),!0;return!1},"reclaimOldestIdle");return{admitSession(s){if(r.maxSessions>0&&a.size>=r.maxSessions&&!v()){t("session-refused",{sessions:a.size,maxSessions:r.maxSessions,streams:_});try{s.goaway(i)}catch{}try{s.close()}catch{}return!1}const l={open:0};return a.set(s,l),h(s,l),s.once("close",()=>{y(l),a.delete(s)}),!0},admitStream(s,l){const d=a.get(s);if(!d)return!0;if(r.maxStreams>0&&_>=r.maxStreams){t("stream-refused",{streams:_,maxStreams:r.maxStreams,sessions:a.size});try{l.close(c)}catch{}return!1}return _+=1,d.open+=1,y(d),l.once("close",()=>{_=Math.max(0,_-1),d.open=Math.max(0,d.open-1),d.open===0&&a.has(s)&&h(s,d)}),!0},dispose(){for(const s of a.values())y(s);a.clear()}}}T(createH2Governor,"createH2Governor"),o(createH2Governor,"createH2Governor");async function startGateway(r,t){const i=r.https!==!1,c=r.ports?.length?r.ports:[443],a=r.scanTtlMs??1500,_=r.startupTimeoutMs??12e4,y=r.sweepMs??1e4,h=r.trafficFlushMs??5e3;let v=!1;const s=process.env.LENSMCP_EVENT_FILE??path.join(t.root,".lensmcp","events.jsonl"),l=(0,own_log_1.startOwnLogRotation)(t.root),d=(0,scope_1.readLensScope)(t.root),E=d.key,M=d.sockNs!==E?d.sockNs:void 0,x=t.projectsConfigurations?.projects??{},b=(0,discovery_1.projectSourceRoots)(t.root,x,t.projectDependencies),{routes:j,services:P,instance:Q}=(0,discovery_1.discoverRoutes)(t.root,x,void 0,E,b,M,d.instance);if(j.length===0)throw new Error("[gateway] no project declares a `cluster` field \u2014 nothing to route.");const K=new route_registry_1.RouteRegistry;K.register({wsKey:E,...M?{sockNs:M}:{},root:t.root,routes:j,services:P,...Q?{instance:Q}:{}});const R=[];let Z=o(()=>{},"refreshCert"),U=o(()=>{},"warmEdgeVerifiers");const ee=o(()=>{(0,route_registry_1.rebuildRoutesInPlace)(R,K),Z(),U()},"rebuildRoutes");ee();const{serviceKeys:le,keyToProject:ue,keyToWorkspace:de}=(0,service_keys_1.loadServiceKeys)(t.root,P,E),pe=(0,client_ip_1.createClientIpResolver)({...(0,client_ip_1.clientIpOptionsFromEnv)(),...r.trustProxyHops!==void 0?{trustProxyHops:r.trustProxyHops}:{},...r.clientIpHeader?{clientIpHeader:r.clientIpHeader}:{},...r.clientIpTrustedProxies?{clientIpTrustedProxies:r.clientIpTrustedProxies}:{}}),g={root:t.root,wsKey:E,...M?{sockNs:M}:{},eventFile:s,routes:R,registry:K,rebuildRoutes:ee,services:P,serviceKeys:le,keyToProject:ue,keyToWorkspace:de,clientIp:pe,bodyStallMs:r.bodyStallMs??types_1.BODY_STALL_MS,scanTtlMs:a,startupTimeoutMs:_,sweepMs:y,trafficFlushMs:h,https:i,projectRoots:Object.fromEntries(Object.entries(t.projectsConfigurations?.projects??{}).map(([e,n])=>[e,path.resolve(t.root,n.root)])),projectSourceRoots:b,...t.projectGraphStore?{graphStores:(0,project_graph_store_1.createProjectGraphStores)(t.projectGraphStore)}:{},...r.sharedCompiler!==void 0?{sharedCompiler:r.sharedCompiler}:{},stopped:o(()=>v,"stopped")};(0,process_name_1.prelinkProcessNames)((0,process_names_1.gatewayProcessNames)(P,{runner:(0,serve_launch_1.serveRunnerEnabled)()&&!!t.projectGraphStore,sharedBuild:(0,protocol_1.sharedCompilerEnabled)(process.env,r.sharedCompiler),label:E||path.basename(t.root),lens:{key:E,dashboard:r.dashboard!==!1,mcp:r.mcp!==!1&&j.some(e=>!!e.lens)}}));const m=(0,observability_1.createObservability)(s,{trafficFlushMs:h}),_e=createClientErrorLogGate(),L=h2BudgetFromEnv(),ge=createClientErrorLogGate(3e4),D=createH2Governor(L,(e,n)=>{ge(e)&&console.warn(`[gateway] h2 aggregate budget: ${e} (${JSON.stringify(n)}). These are the FRONT DOOR's ceilings, not an upstream's \u2014 raise/disable with LENSMCP_MAX_H2_SESSIONS / LENSMCP_MAX_H2_STREAMS / LENSMCP_H2_IDLE_MS / LENSMCP_MAX_CONNECTIONS (0 = unbounded).`),m.emit(e==="session-reclaimed"?"info":"warning",`h2 aggregate budget: ${e}`,`gateway-h2-budget:${e}`,{kind:"h2-aggregate-budget",event:e,project:"gateway",...n})}),A=(0,lifecycle_1.createServiceLayer)(g,m),O=(0,lens_children_1.createLensChildren)(g,m,r),F=new Map;let re=o(()=>{},"ownBooted");const Se=new Promise(e=>re=e),te=(0,warm_start_1.createWorktreeWarmer)({rt:g,obs:m,ready:Se,ensureUp:o(e=>A.ensureUp(e),"ensureUp")});let B;if(r.middleware){const e=require(path.resolve(t.root,r.middleware));B=e.default??e}const Y=(0,workspace_middleware_1.createWorkspaceMiddleware)({ownerKey:E,...B?{owner:B}:{},policy:(0,workspace_middleware_1.guestMiddlewarePolicy)()}),I=process.env.LENSMCP_GW_ACT==="0"?void 0:(0,act_1.createActStore)({dir:path.join(path.dirname((0,control_1.controlSocketPath)()),"identities")});let $;try{$=(0,control_1.startControlServer)({rt:g,killService:o((e,n)=>A.killSpawned(e,n),"killService"),restartService:o((e,n)=>A.restartService(e,n),"restartService"),ensureService:o(e=>A.ensureUp(e),"ensureService"),...I?{act:I}:{},onRegister:o(async e=>{retirePreviousWorkspace(F,e.wsKey,"re-registered"),Y.set(e.wsKey,e.root,e.middleware);const n=await O.hostWorkspaceDashboard(e),w=await O.hostWorkspaceApps(e);F.set(e.wsKey,()=>{try{n?.()}finally{w()}}),te.onRegister(e)},"onRegister"),onUnregister:o(e=>{te.onUnregister(e.wsKey),retirePreviousWorkspace(F,e.wsKey,"unregistered"),Y.drop(e.wsKey),A.stopBuildHost(e.root)},"onUnregister")})}catch(e){console.warn("[gateway] control plane failed to start (multi-workspace register disabled):",e.message)}const H=(0,proxy_1.createProxy)(g,m,A),W=buildEdgeVerifierPool(()=>g.routes),se=(0,auth_1.createEdgeAuth)(g,m,W,I),he=o(e=>{const n=e.route?.wsKey,w=Y.for(n);if(w)try{const u=n?g.registry.get(n):void 0;w(e.req,{...n?{wsKey:n}:{},...u?{root:u.root}:{},env:u?.instance?.env??{}})}catch(u){m.traceStep(e.trace,"auth",{mode:"middleware",ok:!1,reason:u.message}),e.route&&m.finishTrace(e.trace,e.route.project,401),e.sendError(401,"unauthorized",`Gateway middleware rejected the request: ${u.message}`)}},"builtinMiddleware"),oe=(0,hooks_1.createHooks)(withBuiltinHooks(r.hooks??{},{keyWait:edgeKeyWait(W),middleware:he})),ne=(0,handler_1.createHandler)({rt:g,obs:m,auth:se,proxy:H,hooks:oe}),ae=(0,upgrade_1.createUpgrade)({rt:g,auth:se,proxy:H,hooks:oe,obs:m}),Ee=m.startEdgeFlusher(),me=A.startSweeper(),ie=(0,source_poller_1.createSourcePoller)({services:o(()=>g.registry.allServices(),"services"),serviceKey:o(e=>g.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)],"serviceKey"),emit:o((e,n,w,u,S)=>m.emit(n,w,u,{wsKey:e.wsKey,...S},void 0,e.wsKey===g.wsKey?void 0:path.join(e.root,".lensmcp","events.jsonl")),"emit"),scanTtlMs:a,stopped:o(()=>v,"stopped")});(0,source_poller_1.sourcesEnabled)()&&ie.start(),await O.bootSingletonsAndApps();const ce=P.filter(e=>e.decl.eager).map(e=>A.ensureUp(e));Promise.allSettled(ce).then(()=>setTimeout(re,ce.length?2e4:0).unref());let k,X,q;if(i){const e=require("../../../basic-ssl"),n=(0,service_keys_1.gatewayCacheDir)(t.root);q=o(()=>e.getCertificateSync(n,"gateway",g.routes.map(w=>w.host).filter(w=>!!w)),"mintCert"),k=q(),X=e.caCertPath()}const J=[],G=[],z=r.maxConnections??resolveMaxConnections(process.env.LENSMCP_MAX_CONNECTIONS);for(const e of c){const n=k?require("node:http2").createSecureServer({key:k,cert:k,allowHTTP1:!0,maxSessionMemory:512,settings:{maxConcurrentStreams:512},peerMaxConcurrentStreams:512,streamResetBurst:r.h2ResetBurst??types_1.GATEWAY_H2_RESET_BURST,streamResetRate:r.h2ResetRate??types_1.GATEWAY_H2_RESET_RATE},(u,S)=>ne(u,S)):http.createServer(ne);n.on("upgrade",ae),n.on("error",u=>console.error(`[gateway] port ${e}: ${u.code}`)),n.on("connection",guardInboundSocket),n.on("secureConnection",guardInboundSocket),n.on("tlsClientError",(u,S)=>S.destroy()),n.on("clientError",(u,S)=>{const p=u,f=classifyClientError(p.code,p.message);if(f.report&&_e(f.cause)){const N=S.remoteAddress??"unknown",C=f.status===null?"dropped":String(f.status);console.warn(`[gateway] inbound H1 clientError from ${N}: ${f.cause} (${p.code??p.message}${p.bytesParsed!==void 0?`, bytesParsed=${p.bytesParsed}`:""}) \u2192 ${C}. This status is the GATEWAY's, not the upstream service's.`),m.emit("warning",`inbound clientError: ${f.cause} \u2192 ${C}`,`gateway-client-error:${f.cause}`,{kind:"gateway-client-error",cause:f.cause,code:p.code??null,status:f.status,peer:N,bytesParsed:p.bytesParsed??null})}f.status!==null&&S.writable&&S.end(`HTTP/1.1 ${f.status} ${CLIENT_ERROR_REASON[f.status]}\r
|
|
1
|
+
"use strict";var fe=Object.defineProperty;var T=(r,t)=>fe(r,"name",{value:t,configurable:!0});var ye=Object.defineProperty,o=T((r,t)=>ye(r,"name",{value:t,configurable:!0}),"o");Object.defineProperty(exports,"__esModule",{value:!0}),exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT=exports.GATEWAY_MAX_H2_STREAMS_DEFAULT=exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT=exports.GATEWAY_MAX_CONNECTIONS_DEFAULT=exports.BENIGN_SOCKET_ERRNOS=void 0,exports.buildEdgeVerifierPool=buildEdgeVerifierPool,exports.edgeKeyWait=edgeKeyWait,exports.withBuiltinHooks=withBuiltinHooks,exports.classifyClientError=classifyClientError,exports.createClientErrorLogGate=createClientErrorLogGate,exports.retirePreviousWorkspace=retirePreviousWorkspace,exports.resolveMaxConnections=resolveMaxConnections,exports.h2BudgetFromEnv=h2BudgetFromEnv,exports.createH2Governor=createH2Governor,exports.startGateway=startGateway;const tslib_1=require("tslib"),http=tslib_1.__importStar(require("node:http")),path=tslib_1.__importStar(require("node:path")),discovery_1=require("./discovery"),scope_1=require("./scope"),route_registry_1=require("./route-registry"),act_1=require("./act"),control_1=require("./control"),workspace_middleware_1=require("./workspace-middleware"),workspace_registry_1=require("./workspace-registry"),service_keys_1=require("./service-keys"),source_poller_1=require("./source-poller"),client_ip_1=require("../client-ip"),workspace_log_1=require("./workspace-log"),own_log_1=require("./own-log"),observability_1=require("./observability"),lifecycle_1=require("./lifecycle"),process_names_1=require("./process-names"),project_graph_store_1=require("./project-graph-store"),serve_launch_1=require("./serve-launch"),process_name_1=require("../../../process-name"),protocol_1=require("../../../shared-build/protocol"),lens_children_1=require("./lens-children"),proxy_1=require("./proxy"),auth_1=require("./auth"),hooks_1=require("./hooks"),handler_1=require("./handler"),upgrade_1=require("./upgrade"),jwks_verify_1=require("../jwks-verify"),manifest_1=require("../manifest"),edge_1=require("./edge"),warm_start_1=require("./warm-start"),types_1=require("./types");function insecureLocalFetch(r,t){return new Promise((i,c)=>{const a=(r.startsWith("https:")?require("node:https"):require("node:http")).request(r,{method:"GET",headers:t?.headers,rejectUnauthorized:!1},_=>{const m=[];_.on("data",E=>m.push(E)),_.on("error",c),_.on("end",()=>{const E=_.statusCode??0,A=Buffer.concat(m).toString("utf8");i({ok:E>=200&&E<300,status:E,json:o(async()=>JSON.parse(A),"json")})})});a.on("error",c),t?.signal?.addEventListener("abort",()=>a.destroy(new Error(`JWKS ${r} \u2192 aborted`)),{once:!0}),a.end()})}T(insecureLocalFetch,"insecureLocalFetch"),o(insecureLocalFetch,"insecureLocalFetch");function buildEdgeVerifierPool(r,t={}){const i=process.env.LENSMCP_GW_JWKS_URL,c=process.env.LENSMCP_GW_JWT_ISS,a=new Map;let _,m=!1;const E=o((s,l)=>{console.log(`[gateway] edge JWT: verifying via JWKS ${s}${l}`);const d=(0,jwks_verify_1.createJwksVerifier)({jwksUrl:s,...c?{issuer:c}:{},fetchImpl:t.fetchImpl??insecureLocalFetch});let y=!1;return d.warm({...t.warm,onError:o((M,x,b)=>{y=!0,x===1&&b!==void 0&&console.warn(`[gateway] edge JWKS ${s} not loaded yet (${M.message}) \u2014 retrying in the background`),b===void 0&&console.warn(`[gateway] edge JWKS ${s} still not loaded after ${x} attempts (${M.message}) \u2014 the first token that needs it fetches it`)},"onError")}).then(M=>{M&&y&&console.log(`[gateway] edge JWKS ${s} loaded`)}),d},"make"),A=o(s=>{let l=a.get(s);return l||(l=E(`https://auth.${s}/jwks.json`,` (auto-derived for apex ${s})`),a.set(s,l)),l},"forApex");return{for(s){if(i)return _??=E(i,"");const l=s.split(":")[0].toLowerCase();let d;for(const y of idpApexes(r()))(l===y||l.endsWith(`.${y}`))&&(!d||y.length>d.length)&&(d=y);return d?A(d):void 0},warm(){if(!m){if(i){_??=E(i,"");return}for(const s of idpApexes(r()))A(s)}},stop(){m=!0,_?.stop();for(const s of a.values())s.stop()}}}T(buildEdgeVerifierPool,"buildEdgeVerifierPool"),o(buildEdgeVerifierPool,"buildEdgeVerifierPool");function idpApexes(r){const t=new Set;for(const i of r){const c=i.host;typeof c!="string"||!/^auth\./.test(c)||c.startsWith("internal.")||t.add(c.slice(5))}return t}T(idpApexes,"idpApexes"),o(idpApexes,"idpApexes");function edgeKeyWait(r){return async t=>{const i=t.route;if(!i?.auth||i.internal)return;const{req:c}=t,a=Array.isArray(c.headers.authorization)?c.headers.authorization[0]:c.headers.authorization,_=String(a??"");if(!_.startsWith("Bearer "))return;const m=c.url??"/";if((0,manifest_1.authRuleFor)(i.auth,(0,edge_1.canonicalPath)(m)??m.split("?")[0],(c.method??"GET").toUpperCase()).mode!=="jwt")return;const E=String(c.headers.host??"").split(":")[0].toLowerCase();await r.for(E)?.awaitKey(_.slice(7))}}T(edgeKeyWait,"edgeKeyWait"),o(edgeKeyWait,"edgeKeyWait");function withBuiltinHooks(r,t){return{...r,beforeAuth:[...r.beforeAuth??[],t.keyWait],afterAuth:[t.middleware,...r.afterAuth??[]]}}T(withBuiltinHooks,"withBuiltinHooks"),o(withBuiltinHooks,"withBuiltinHooks"),exports.BENIGN_SOCKET_ERRNOS=new Set(["ECONNRESET","EPIPE","ETIMEDOUT","ECONNABORTED","ECANCELED"]);function classifyClientError(r,t){return r&&exports.BENIGN_SOCKET_ERRNOS.has(r)?{status:null,report:!1,cause:"client-abort"}:r==="HPE_INVALID_EOF_STATE"?{status:null,report:!1,cause:"truncated-at-eof"}:r==="ERR_HTTP_REQUEST_TIMEOUT"?{status:408,report:!0,cause:"request-timeout"}:r==="HPE_HEADER_OVERFLOW"?{status:431,report:!0,cause:"header-overflow"}:r?.startsWith("HPE_")?{status:400,report:!0,cause:"parse-error"}:{status:400,report:!0,cause:r??t??"unknown"}}T(classifyClientError,"classifyClientError"),o(classifyClientError,"classifyClientError");const CLIENT_ERROR_REASON={400:"Bad Request",408:"Request Timeout",431:"Request Header Fields Too Large"};function createClientErrorLogGate(r=1e4){const t=new Map;return i=>{const c=Date.now(),a=t.get(i)??0;return c-a<r?!1:(t.set(i,c),!0)}}T(createClientErrorLogGate,"createClientErrorLogGate"),o(createClientErrorLogGate,"createClientErrorLogGate");function guardInboundSocket(r){r.on("error",t=>{exports.BENIGN_SOCKET_ERRNOS.has(t.code??"")||console.warn(`[gateway] inbound socket error (${t.code??t.message}) \u2014 dropping this connection only`),r.destroy()})}T(guardInboundSocket,"guardInboundSocket"),o(guardInboundSocket,"guardInboundSocket");function retirePreviousWorkspace(r,t,i){const c=r.get(t);if(!c)return!1;r.delete(t),i==="re-registered"&&console.log(`[gateway] workspace '${t}' re-registered \u2014 retiring the children of its previous registration first.`);try{c()}catch(a){console.warn(`[gateway] could not fully retire the previous registration of '${t}' (${i}): ${a.message}`)}return!0}T(retirePreviousWorkspace,"retirePreviousWorkspace"),o(retirePreviousWorkspace,"retirePreviousWorkspace"),exports.GATEWAY_MAX_CONNECTIONS_DEFAULT=512,exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT=64,exports.GATEWAY_MAX_H2_STREAMS_DEFAULT=4096,exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT=3e5;function parseBudget(r,t){if(r===void 0||r.trim()==="")return t;const i=Number(r);return!Number.isFinite(i)||i<0?t:Math.floor(i)}T(parseBudget,"parseBudget"),o(parseBudget,"parseBudget");function resolveMaxConnections(r){return parseBudget(r,exports.GATEWAY_MAX_CONNECTIONS_DEFAULT)}T(resolveMaxConnections,"resolveMaxConnections"),o(resolveMaxConnections,"resolveMaxConnections");function h2BudgetFromEnv(r=process.env){return{maxSessions:parseBudget(r.LENSMCP_MAX_H2_SESSIONS,exports.GATEWAY_MAX_H2_SESSIONS_DEFAULT),maxStreams:parseBudget(r.LENSMCP_MAX_H2_STREAMS,exports.GATEWAY_MAX_H2_STREAMS_DEFAULT),idleSessionMs:parseBudget(r.LENSMCP_H2_IDLE_MS,exports.GATEWAY_H2_IDLE_SESSION_MS_DEFAULT)}}T(h2BudgetFromEnv,"h2BudgetFromEnv"),o(h2BudgetFromEnv,"h2BudgetFromEnv");function createH2Governor(r,t){const{NGHTTP2_ENHANCE_YOUR_CALM:i,NGHTTP2_REFUSED_STREAM:c}=require("node:http2").constants,a=new Map;let _=0;const m=o(s=>{s.idle&&(clearTimeout(s.idle),s.idle=void 0)},"disarm"),E=o((s,l)=>{m(l),!(r.idleSessionMs<=0||l.open>0)&&(l.idle=setTimeout(()=>{s.destroyed||s.closed||(t("session-reclaimed",{idleMs:r.idleSessionMs,sessions:a.size}),a.delete(s),s.close())},r.idleSessionMs),l.idle.unref?.())},"arm"),A=o(()=>{for(const[s,l]of a)if(!(l.open>0||s.destroyed))return m(l),a.delete(s),t("session-reclaimed",{reason:"cap",sessions:a.size+1,maxSessions:r.maxSessions}),s.close(),!0;return!1},"reclaimOldestIdle");return{admitSession(s){if(r.maxSessions>0&&a.size>=r.maxSessions&&!A()){t("session-refused",{sessions:a.size,maxSessions:r.maxSessions,streams:_});try{s.goaway(i)}catch{}try{s.close()}catch{}return!1}const l={open:0};return a.set(s,l),E(s,l),s.once("close",()=>{m(l),a.delete(s)}),!0},admitStream(s,l){const d=a.get(s);if(!d)return!0;if(r.maxStreams>0&&_>=r.maxStreams){t("stream-refused",{streams:_,maxStreams:r.maxStreams,sessions:a.size});try{l.close(c)}catch{}return!1}return _+=1,d.open+=1,m(d),l.once("close",()=>{_=Math.max(0,_-1),d.open=Math.max(0,d.open-1),d.open===0&&a.has(s)&&E(s,d)}),!0},dispose(){for(const s of a.values())m(s);a.clear()}}}T(createH2Governor,"createH2Governor"),o(createH2Governor,"createH2Governor");async function startGateway(r,t){const i=r.https!==!1,c=r.ports?.length?r.ports:[443],a=r.scanTtlMs??1500,_=r.startupTimeoutMs??12e4,m=r.sweepMs??1e4,E=r.trafficFlushMs??5e3;let A=!1;const s=process.env.LENSMCP_EVENT_FILE??path.join(t.root,".lensmcp","events.jsonl"),l=(0,own_log_1.startOwnLogRotation)(t.root),d=(0,scope_1.readLensScope)(t.root),y=d.key,M=d.sockNs!==y?d.sockNs:void 0,x=t.projectsConfigurations?.projects??{},b=(0,discovery_1.projectSourceRoots)(t.root,x,t.projectDependencies),{routes:G,services:P,instance:Z}=(0,discovery_1.discoverRoutes)(t.root,x,void 0,y,b,M,d.instance);if(G.length===0)throw new Error("[gateway] no project declares a `cluster` field \u2014 nothing to route.");const K=new route_registry_1.RouteRegistry;K.register({wsKey:y,...M?{sockNs:M}:{},root:t.root,routes:G,services:P,...Z?{instance:Z}:{}});const R=[];let ee=o(()=>{},"refreshCert"),U=o(()=>{},"warmEdgeVerifiers");const re=o(()=>{(0,route_registry_1.rebuildRoutesInPlace)(R,K),ee(),U()},"rebuildRoutes");re();const{serviceKeys:le,keyToProject:ue,keyToWorkspace:de}=(0,service_keys_1.loadServiceKeys)(t.root,P,y),pe=(0,client_ip_1.createClientIpResolver)({...(0,client_ip_1.clientIpOptionsFromEnv)(),...r.trustProxyHops!==void 0?{trustProxyHops:r.trustProxyHops}:{},...r.clientIpHeader?{clientIpHeader:r.clientIpHeader}:{},...r.clientIpTrustedProxies?{clientIpTrustedProxies:r.clientIpTrustedProxies}:{}}),S={root:t.root,wsKey:y,...M?{sockNs:M}:{},eventFile:s,routes:R,registry:K,rebuildRoutes:re,services:P,serviceKeys:le,keyToProject:ue,keyToWorkspace:de,clientIp:pe,bodyStallMs:r.bodyStallMs??types_1.BODY_STALL_MS,scanTtlMs:a,startupTimeoutMs:_,sweepMs:m,trafficFlushMs:E,https:i,projectRoots:Object.fromEntries(Object.entries(t.projectsConfigurations?.projects??{}).map(([e,n])=>[e,path.resolve(t.root,n.root)])),projectSourceRoots:b,...t.projectGraphStore?{graphStores:(0,project_graph_store_1.createProjectGraphStores)(t.projectGraphStore)}:{},...r.sharedCompiler!==void 0?{sharedCompiler:r.sharedCompiler}:{},stopped:o(()=>A,"stopped")};(0,process_name_1.prelinkProcessNames)((0,process_names_1.gatewayProcessNames)(P,{runner:(0,serve_launch_1.serveRunnerEnabled)()&&!!t.projectGraphStore,sharedBuild:(0,protocol_1.sharedCompilerEnabled)(process.env,r.sharedCompiler),label:y||path.basename(t.root),lens:{key:y,dashboard:r.dashboard!==!1,mcp:r.mcp!==!1&&G.some(e=>!!e.lens)}}));const w=(0,observability_1.createObservability)(s,{trafficFlushMs:E}),_e=createClientErrorLogGate(),L=h2BudgetFromEnv(),Se=createClientErrorLogGate(3e4),D=createH2Governor(L,(e,n)=>{Se(e)&&console.warn(`[gateway] h2 aggregate budget: ${e} (${JSON.stringify(n)}). These are the FRONT DOOR's ceilings, not an upstream's \u2014 raise/disable with LENSMCP_MAX_H2_SESSIONS / LENSMCP_MAX_H2_STREAMS / LENSMCP_H2_IDLE_MS / LENSMCP_MAX_CONNECTIONS (0 = unbounded).`),w.emit(e==="session-reclaimed"?"info":"warning",`h2 aggregate budget: ${e}`,`gateway-h2-budget:${e}`,{kind:"h2-aggregate-budget",event:e,project:"gateway",...n})}),v=(0,lifecycle_1.createServiceLayer)(S,w),O=(0,lens_children_1.createLensChildren)(S,w,r),F=new Map;let te=o(()=>{},"ownBooted");const ge=new Promise(e=>te=e),B=(0,warm_start_1.createWorktreeWarmer)({rt:S,obs:w,ready:ge,ensureUp:o(e=>v.ensureUp(e),"ensureUp")});let Y;if(r.middleware){const e=require(path.resolve(t.root,r.middleware));Y=e.default??e}const X=(0,workspace_middleware_1.createWorkspaceMiddleware)({ownerKey:y,...Y?{owner:Y}:{},policy:(0,workspace_middleware_1.guestMiddlewarePolicy)()}),I=process.env.LENSMCP_GW_ACT==="0"?void 0:(0,act_1.createActStore)({dir:path.join(path.dirname((0,control_1.controlSocketPath)()),"identities")});let $;try{$=(0,control_1.startControlServer)({rt:S,killService:o((e,n)=>v.killSpawned(e,n),"killService"),restartService:o((e,n)=>v.restartService(e,n),"restartService"),ensureService:o(e=>v.ensureUp(e),"ensureService"),startTurn:o(e=>B.take(e),"startTurn"),emitService:o((e,n,g,u)=>v.emitSvc(e,n,g,`cluster-service:${e.project}`,u),"emitService"),...I?{act:I}:{},onRegister:o(async e=>{retirePreviousWorkspace(F,e.wsKey,"re-registered"),X.set(e.wsKey,e.root,e.middleware);const n=await O.hostWorkspaceDashboard(e),g=await O.hostWorkspaceApps(e);F.set(e.wsKey,()=>{try{n?.()}finally{g()}}),B.onRegister(e)},"onRegister"),onUnregister:o(e=>{B.onUnregister(e.wsKey),retirePreviousWorkspace(F,e.wsKey,"unregistered"),X.drop(e.wsKey),v.stopBuildHost(e.root)},"onUnregister")})}catch(e){console.warn("[gateway] control plane failed to start (multi-workspace register disabled):",e.message)}const H=(0,proxy_1.createProxy)(S,w,v),W=buildEdgeVerifierPool(()=>S.routes),se=(0,auth_1.createEdgeAuth)(S,w,W,I),he=o(e=>{const n=e.route?.wsKey,g=X.for(n);if(g)try{const u=n?S.registry.get(n):void 0;g(e.req,{...n?{wsKey:n}:{},...u?{root:u.root}:{},env:u?.instance?.env??{}})}catch(u){w.traceStep(e.trace,"auth",{mode:"middleware",ok:!1,reason:u.message}),e.route&&w.finishTrace(e.trace,e.route.project,401),e.sendError(401,"unauthorized",`Gateway middleware rejected the request: ${u.message}`)}},"builtinMiddleware"),oe=(0,hooks_1.createHooks)(withBuiltinHooks(r.hooks??{},{keyWait:edgeKeyWait(W),middleware:he})),ne=(0,handler_1.createHandler)({rt:S,obs:w,auth:se,proxy:H,hooks:oe}),ae=(0,upgrade_1.createUpgrade)({rt:S,auth:se,proxy:H,hooks:oe,obs:w}),Ee=w.startEdgeFlusher(),me=v.startSweeper(),ie=(0,source_poller_1.createSourcePoller)({services:o(()=>S.registry.allServices(),"services"),serviceKey:o(e=>S.serviceKeys[(0,service_keys_1.serviceKeyId)(e.wsKey,e.project)],"serviceKey"),emit:o((e,n,g,u,h)=>w.emit(n,g,u,{wsKey:e.wsKey,...h},void 0,e.wsKey===S.wsKey?void 0:path.join(e.root,".lensmcp","events.jsonl")),"emit"),scanTtlMs:a,stopped:o(()=>A,"stopped")});(0,source_poller_1.sourcesEnabled)()&&ie.start(),await O.bootSingletonsAndApps();const ce=P.filter(e=>e.decl.eager).map(e=>v.ensureUp(e));Promise.allSettled(ce).then(()=>setTimeout(te,ce.length?2e4:0).unref());let k,J,q;if(i){const e=require("../../../basic-ssl"),n=(0,service_keys_1.gatewayCacheDir)(t.root);q=o(()=>e.getCertificateSync(n,"gateway",S.routes.map(g=>g.host).filter(g=>!!g)),"mintCert"),k=q(),J=e.caCertPath()}const z=[],j=[],V=r.maxConnections??resolveMaxConnections(process.env.LENSMCP_MAX_CONNECTIONS);for(const e of c){const n=k?require("node:http2").createSecureServer({key:k,cert:k,allowHTTP1:!0,maxSessionMemory:512,settings:{maxConcurrentStreams:512},peerMaxConcurrentStreams:512,streamResetBurst:r.h2ResetBurst??types_1.GATEWAY_H2_RESET_BURST,streamResetRate:r.h2ResetRate??types_1.GATEWAY_H2_RESET_RATE},(u,h)=>ne(u,h)):http.createServer(ne);n.on("upgrade",ae),n.on("error",u=>console.error(`[gateway] port ${e}: ${u.code}`)),n.on("connection",guardInboundSocket),n.on("secureConnection",guardInboundSocket),n.on("tlsClientError",(u,h)=>h.destroy()),n.on("clientError",(u,h)=>{const p=u,f=classifyClientError(p.code,p.message);if(f.report&&_e(f.cause)){const N=h.remoteAddress??"unknown",C=f.status===null?"dropped":String(f.status);console.warn(`[gateway] inbound H1 clientError from ${N}: ${f.cause} (${p.code??p.message}${p.bytesParsed!==void 0?`, bytesParsed=${p.bytesParsed}`:""}) \u2192 ${C}. This status is the GATEWAY's, not the upstream service's.`),w.emit("warning",`inbound clientError: ${f.cause} \u2192 ${C}`,`gateway-client-error:${f.cause}`,{kind:"gateway-client-error",cause:f.cause,code:p.code??null,status:f.status,peer:N,bytesParsed:p.bytesParsed??null})}f.status!==null&&h.writable&&h.end(`HTTP/1.1 ${f.status} ${CLIENT_ERROR_REASON[f.status]}\r
|
|
2
2
|
\r
|
|
3
|
-
`),
|
|
3
|
+
`),h.destroy()}),n.keepAliveTimeout=types_1.GATEWAY_KEEPALIVE_TIMEOUT_MS,n.headersTimeout=r.headersTimeoutMs??types_1.GATEWAY_HEADERS_TIMEOUT_MS,V>0&&(n.maxConnections=V);const g=r.connectionsCheckingIntervalMs??types_1.GATEWAY_CONN_CHECK_MS;if(g>0&&(n.connectionsCheckingInterval=g),k!=null){const u=n,{NGHTTP2_CANCEL:h}=require("node:http2").constants;u.on("session",p=>{if(!D.admitSession(p)){p.on("error",()=>{});return}let f=0,N=0;p.on("stream",C=>{D.admitStream(p,C)&&C.once("close",()=>{p.destroyed?N+=1:C.rstCode===h&&(f+=1)})}),p.once("close",()=>{N!==0&&(console.warn(`[gateway] h2 session closed with ${N} stream(s) in flight after ${f} client reset(s) \u2014 every in-flight fetch on that connection failed at once (ERR_HTTP2_PROTOCOL_ERROR in the browser). Reset budget: LENSMCP_H2_RESET_BURST/LENSMCP_H2_RESET_RATE (issues/closed/foodguard-14).`),w.emit("warning",`h2 session lost with ${N} stream(s) in flight`,"gateway-h2-session-lost",{kind:"h2-session-lost",project:"gateway",inflight:N,clientResets:f}))}),p.on("error",C=>{const we=C.code??C.message;console.warn(`[gateway] h2 session error (${we}) \u2014 dropping this session only`),p.destroyed||p.destroy()})}),u.on("sessionError",p=>{console.warn(`[gateway] h2 sessionError: ${p.code??p.message}`)})}await new Promise(u=>{n.listen(e,()=>{const h=n.address()?.port??e;j.push(h),console.log(`[gateway] Listening on ${k?"https":"http"}://localhost:${h}`),u()})}),z.push(n)}q&&(ee=o(()=>{const e=q();for(const n of z)n.setSecureContext?.({key:e,cert:e})},"refreshCert")),console.log("[gateway] Routes:");for(const e of R){const n=e.pool?`pods@${e.pool.dir}`:e.target;console.log(`[gateway] ${e.host??"(default)"} \u2192 ${n}${e.prependPrefix?` (+${e.prependPrefix})`:""}${e.internal?" [internal: no middleware]":""} [${e.project}]`)}J&&console.log(`[gateway] CA: ${J} \u2014 one-time setup via the trust executor.`),U=o(()=>W.warm(),"warmEdgeVerifiers"),U();const Q=o(e=>e>0?String(e):"unbounded","unbounded");return console.log(`[gateway] front-door budget: \u2264${Q(V)} connections, \u2264${Q(L.maxSessions)} h2 sessions (each \u2264512 MB session memory, \u2264512 streams), \u2264${Q(L.maxStreams)} open streams in total, idle h2 session reclaimed after ${L.idleSessionMs>0?`${Math.round(L.idleSessionMs/1e3)}s`:"never"}. Override: LENSMCP_MAX_CONNECTIONS / LENSMCP_MAX_H2_SESSIONS / LENSMCP_MAX_H2_STREAMS / LENSMCP_H2_IDLE_MS (0 = unbounded).`),w.emit("info",`gateway up: ${R.length} routes on ${j.join("/")}`,"cluster-gateway",{kind:"gateway-up",project:"gateway",ports:j,routes:R.map(e=>({host:e.host??"(default)",project:e.project,mode:e.pool?"pods":"tcp",internal:!!e.internal,prependPrefix:e.prependPrefix}))}),$&&process.env.LENSMCP_GUEST_REPLAY!=="0"&&$.replay().catch(e=>console.warn("[gateway] guest replay failed:",e.message)),{ports:j,routes:R,services:P,stop:o(async()=>{if(!A){A=!0,clearInterval(Ee),clearInterval(me),ie.stop(),w.emit("info","gateway down","cluster-gateway",{kind:"gateway-down",project:"gateway"}),W.stop(),$?.close(),I?.flush(),(0,workspace_registry_1.unregisterWorkspace)(y);for(const e of P)v.killSpawned(e,"gateway shutdown");v.stopRetiringTrees(),await v.stopBuildHost(),await v.finishDyingTrees(),await O.reapAll(),ae.closeTunnels(),await Promise.all(z.map(e=>new Promise(n=>{let g=!1;const u=o(()=>{g||(g=!0,n())},"done"),h=e;e.close(()=>u());try{h.closeIdleConnections?.()}catch{}setTimeout(()=>{try{h.closeAllConnections?.()}catch{}u()},types_1.GATEWAY_STOP_GRACE_MS).unref?.()}))),H.closeProxy(),H.destroyAgents(),D.dispose(),(0,workspace_log_1.closeWorkspaceLogs)(),l()}},"stop"),...S.lensSupervisor?{lensSupervisor:S.lensSupervisor}:{},...S.graphStores?{graphStores:S.graphStores}:{}}}T(startGateway,"startGateway"),o(startGateway,"startGateway");
|
|
@@ -270,6 +270,17 @@ export interface LensFrontendSupervisor {
|
|
|
270
270
|
/** Ensure a replacement is running and resolve when its route is ready. */
|
|
271
271
|
recover(frontend: LensFrontend, reason: string): Promise<boolean>;
|
|
272
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* What a `POST /awake` did with one service (awake.ts): it was `up` already; it is `queued` behind the machine's one
|
|
275
|
+
* start at a time, or `starting`; it `started`; the memory floor `deferred` it; it `failed` to come up; or it was
|
|
276
|
+
* `skipped` (stopped by hand and not named, or a stop/start/restart of it under way). `reason` says why for the last three.
|
|
277
|
+
*/
|
|
278
|
+
export interface ServiceWake {
|
|
279
|
+
/** When it was asked for (ms). */
|
|
280
|
+
at: number;
|
|
281
|
+
outcome: 'up' | 'queued' | 'starting' | 'started' | 'deferred' | 'failed' | 'skipped';
|
|
282
|
+
reason?: string;
|
|
283
|
+
}
|
|
273
284
|
/**
|
|
274
285
|
* A restart someone asked for — an agent's `service_restart`, `lensmcp restart <service>` — of ONE service of one
|
|
275
286
|
* workspace or worktree. A fresh tree re-reads what the service reads when it starts: its config files, its env, its
|
|
@@ -339,6 +350,19 @@ export interface ServiceCtl {
|
|
|
339
350
|
rollFailure?: string;
|
|
340
351
|
/** The latest restart someone ASKED for (control.ts `POST /restart`, lifecycle.ts `restartService`) — settled or not. */
|
|
341
352
|
restart?: ServiceRestart;
|
|
353
|
+
/**
|
|
354
|
+
* Kept awake until then (ms): the idle sweep stops it no sooner, and its idle window counts from then rather than from
|
|
355
|
+
* a request long ago (control.ts `POST /awake`, awake.ts). A queue worker's work arrives with no request, and stops
|
|
356
|
+
* with its service (issues/closed/foodguard-123).
|
|
357
|
+
*/
|
|
358
|
+
awakeUntil?: number;
|
|
359
|
+
/** When the idle sweep last stopped it, and why — what a preflight says of a required service it finds down. */
|
|
360
|
+
lastIdleStop?: {
|
|
361
|
+
at: number;
|
|
362
|
+
why: string;
|
|
363
|
+
};
|
|
364
|
+
/** The latest `POST /awake` of it, and how its start went ({@link ServiceWake}). */
|
|
365
|
+
wake?: ServiceWake;
|
|
342
366
|
/** The pod GENERATION the current `proc` was spawned as (`LENSMCP_POD_GENERATION`, echoed in `/__lensmcp/alive`). */
|
|
343
367
|
generation?: number;
|
|
344
368
|
/** How the current `proc` was started: the gateway's serve runner, or `nx run` (serve-launch.ts). An `nx run`
|
|
@@ -12,6 +12,16 @@ export declare function warmMinFreeBytes(env?: NodeJS.ProcessEnv): number;
|
|
|
12
12
|
* `undefined` — never idle-stopped — for the daemon's own and other registered workspaces' services, as before.
|
|
13
13
|
*/
|
|
14
14
|
export declare function instanceIdleSec(svc: Pick<ServiceCtl, 'decl'>, isInstance: boolean, env?: NodeJS.ProcessEnv): number | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* The idle window the sweep gives `svc` (lifecycle.ts `sweepOnce`) — {@link instanceIdleSec}, an instance being a
|
|
17
|
+
* registered worktree's service, never the daemon's own. One rule for the sweep and for what `/status` says of it.
|
|
18
|
+
*/
|
|
19
|
+
export declare function serviceIdleSec(rt: {
|
|
20
|
+
readonly root?: string;
|
|
21
|
+
readonly registry?: {
|
|
22
|
+
get(wsKey: string): WorkspaceFragment | undefined;
|
|
23
|
+
};
|
|
24
|
+
}, svc: Pick<ServiceCtl, 'decl' | 'root' | 'wsKey'>, env?: NodeJS.ProcessEnv): number | undefined;
|
|
15
25
|
export interface MemoryProbe {
|
|
16
26
|
platform?: NodeJS.Platform;
|
|
17
27
|
/** `sysctl -n <name>`. */
|
|
@@ -39,6 +49,8 @@ export interface WarmOutcome {
|
|
|
39
49
|
outcome: 'up' | 'failed' | 'deferred' | 'already';
|
|
40
50
|
reason?: string;
|
|
41
51
|
}
|
|
52
|
+
/** Why a start waits, when the memory the system can still give out is under the floor; nothing when it is not (or unread). */
|
|
53
|
+
export declare function belowFloor(free: number | undefined, minFreeBytes: number): string | undefined;
|
|
42
54
|
/** Start `services`' always-warm ones, one at a time, while the floor holds. Never throws. */
|
|
43
55
|
export declare function warmStart(services: readonly ServiceCtl[], deps: WarmStartDeps): Promise<WarmOutcome[]>;
|
|
44
56
|
export interface WorktreeWarmerDeps {
|
|
@@ -61,4 +73,10 @@ export interface WorktreeWarmerDeps {
|
|
|
61
73
|
export declare function createWorktreeWarmer(deps: WorktreeWarmerDeps): {
|
|
62
74
|
onRegister(fragment: WorkspaceFragment): Promise<WarmOutcome[]> | undefined;
|
|
63
75
|
onUnregister(wsKey: string): void;
|
|
76
|
+
/**
|
|
77
|
+
* Run `job` in the machine's one turn — after every warm start before it, and before any after it. A worktree's
|
|
78
|
+
* services woken for a milestone (control.ts `POST /awake`, awake.ts) start here too, so a wake and a registration
|
|
79
|
+
* never start two compilers at once (issues/closed/foodguard-123).
|
|
80
|
+
*/
|
|
81
|
+
take<T>(job: () => Promise<T>): Promise<T>;
|
|
64
82
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";var
|
|
1
|
+
"use strict";var m=Object.defineProperty;var u=(e,r)=>m(e,"name",{value:r,configurable:!0});var f=Object.defineProperty,a=u((e,r)=>f(e,"name",{value:r,configurable:!0}),"a");Object.defineProperty(exports,"__esModule",{value:!0}),exports.DEFAULT_WORKTREE_IDLE_SEC=exports.WORKTREE_IDLE_ENV=exports.DEFAULT_WARM_MIN_FREE_MB=exports.WARM_MIN_FREE_ENV=void 0,exports.warmMinFreeBytes=warmMinFreeBytes,exports.instanceIdleSec=instanceIdleSec,exports.serviceIdleSec=serviceIdleSec,exports.availableMemory=availableMemory,exports.belowFloor=belowFloor,exports.warmStart=warmStart,exports.createWorktreeWarmer=createWorktreeWarmer;const tslib_1=require("tslib"),node_child_process_1=require("node:child_process"),fs=tslib_1.__importStar(require("node:fs")),os=tslib_1.__importStar(require("node:os")),path=tslib_1.__importStar(require("node:path"));exports.WARM_MIN_FREE_ENV="LENSMCP_WARM_MIN_FREE_MB",exports.DEFAULT_WARM_MIN_FREE_MB=4096,exports.WORKTREE_IDLE_ENV="LENSMCP_WORKTREE_IDLE_SEC",exports.DEFAULT_WORKTREE_IDLE_SEC=1800;function warmMinFreeBytes(e=process.env){const r=Number(e[exports.WARM_MIN_FREE_ENV]);return(Number.isFinite(r)&&r>=0?r:exports.DEFAULT_WARM_MIN_FREE_MB)*1024*1024}u(warmMinFreeBytes,"warmMinFreeBytes"),a(warmMinFreeBytes,"warmMinFreeBytes");function instanceIdleSec(e,r,n=process.env){if(e.decl.idleKillSec)return e.decl.idleKillSec;if(!r)return;const o=Number(n[exports.WORKTREE_IDLE_ENV]?.trim()||NaN);return!Number.isFinite(o)||o<0?exports.DEFAULT_WORKTREE_IDLE_SEC:o>0?o:void 0}u(instanceIdleSec,"instanceIdleSec"),a(instanceIdleSec,"instanceIdleSec");function serviceIdleSec(e,r,n=process.env){const o=e.root!==void 0&&r.root!==void 0&&path.resolve(r.root)===path.resolve(e.root);return instanceIdleSec(r,!o&&!!e.registry?.get(r.wsKey)?.instance,n)}u(serviceIdleSec,"serviceIdleSec"),a(serviceIdleSec,"serviceIdleSec");function availableMemory(e={}){const r=e.platform??process.platform;try{if(r==="darwin"){const n=e.sysctl??(t=>(0,node_child_process_1.execFileSync)("sysctl",["-n",t],{encoding:"utf8",timeout:2e3})),o=Number(n("kern.memorystatus_level").trim());return!Number.isFinite(o)||o<0||o>100?void 0:Math.round(o/100*(e.totalmem??os.totalmem)())}if(r==="linux"){const n=(e.readFile??(t=>fs.readFileSync(t,"utf8")))("/proc/meminfo"),o=/^MemAvailable:\s+(\d+)\s+kB/m.exec(n)?.[1];return o===void 0?void 0:Number(o)*1024}}catch{}}u(availableMemory,"availableMemory"),a(availableMemory,"availableMemory");const gb=a(e=>`${(e/1024**3).toFixed(1)} GB`,"gb");function belowFloor(e,r){return e!==void 0&&e<r?`${gb(e)} of memory free, under the ${gb(r)} a warm start keeps (${exports.WARM_MIN_FREE_ENV})`:void 0}u(belowFloor,"belowFloor"),a(belowFloor,"belowFloor");async function warmStart(e,r){const n=[],o=e.filter(t=>t.decl.eager&&!t.frozen);for(let t=0;t<o.length;t++){const c=o[t];if(!r.current(c))break;if(c.proc||c.state!=="down"){n.push({project:c.project,outcome:"already"});continue}const d=r.available(),s=belowFloor(d,r.minFreeBytes);if(s){const p=o.slice(t).filter(i=>!i.proc&&i.state==="down");r.log(`[gateway] warm start stopped for ${c.wsKey}: ${s} \u2014 ${p.map(i=>i.project).join(", ")} start on their first request.`);for(const i of p)r.emit(i,"info",`warm start deferred: ${i.project} \u2014 ${s}`,{kind:"service-warm-deferred",project:i.project,reason:s,availableBytes:d,minFreeBytes:r.minFreeBytes}),n.push({project:i.project,outcome:"deferred",reason:s});return n}const l=await r.ensureUp(c).catch(()=>!1);l&&(c.lastUsed=Date.now()),n.push({project:c.project,outcome:l?"up":"failed"})}return n}u(warmStart,"warmStart"),a(warmStart,"warmStart");function createWorktreeWarmer(e){const r=new Map,n=e.log??(t=>console.log(t));let o=(e.ready??Promise.resolve()).catch(()=>{});return{onRegister(t){if(!t.instance||t.instance.frozen)return;const c=Symbol(t.wsKey);r.set(t.wsKey,c);const d=o.then(()=>new Promise(s=>setTimeout(s,e.delayMs??1e3).unref?.())).then(()=>warmStart(t.services,{ensureUp:e.ensureUp,available:e.available??(()=>availableMemory()),minFreeBytes:warmMinFreeBytes(e.env),current:a(s=>!e.rt.stopped()&&r.get(s.wsKey)===c&&!!e.rt.registry.get(s.wsKey)?.services.includes(s),"current"),emit:a((s,l,p,i)=>e.obs.emit(l,p,`cluster-service:${s.project}`,{wsKey:s.wsKey,...i},void 0,path.join(s.root,".lensmcp","events.jsonl")),"emit"),log:n})).then(s=>{const l=a(p=>s.filter(i=>i.outcome===p).map(i=>i.project),"by");return s.length&&n(`[gateway] warm start of ${t.wsKey}: up ${l("up").join(", ")||"\u2014"}${l("failed").length?`; failed ${l("failed").join(", ")}`:""}${l("deferred").length?`; on first request ${l("deferred").join(", ")}`:""}`),s});return o=d.catch(()=>{}),d},onUnregister(t){r.delete(t)},take(t){const c=o.then(t);return o=c.catch(()=>{}),c}}}u(createWorktreeWarmer,"createWorktreeWarmer"),a(createWorktreeWarmer,"createWorktreeWarmer");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lensmcp/cluster",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.39",
|
|
4
4
|
"description": "Run your Nx workspace as a local production cluster: pods on unix sockets with true HMR, one https gateway with per-project domains, scale-from-zero, autoscale, idle-kill, local-CA TLS — observed by the LensMCP lens.",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"types": "./index.d.ts",
|
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
}
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
-
"@lensmcp/core": "1.25.
|
|
62
|
-
"@lensmcp/node-instrumentation": "1.25.
|
|
63
|
-
"@lensmcp/nx-plugin": "1.25.
|
|
61
|
+
"@lensmcp/core": "1.25.39",
|
|
62
|
+
"@lensmcp/node-instrumentation": "1.25.39",
|
|
63
|
+
"@lensmcp/nx-plugin": "1.25.39",
|
|
64
64
|
"fastify": "^5.8.5",
|
|
65
65
|
"@fastify/reply-from": "^12.6.2",
|
|
66
66
|
"@typescript/native-preview": "^7.0.0-dev.20260707.2",
|