Repository navigation
Expand file tree
/
Copy pathadapter-lease.ts
More file actions
117 lines (109 loc) · 5.02 KB
/
Copy pathadapter-lease.ts
File metadata and controls
117 lines (109 loc) · 5.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/**
* Explicit ownership of one adapter instance for the duration of a launch or
* attach setup.
*
* The registry caps concurrent adapters per language (`maxInstancesPerLanguage`,
* `adapter-registry.ts`) and releases a slot only when the adapter emits
* 'disposed'. Ownership was a *time window* inside `ProxyLauncher.start` guarded
* by a `let adapterOwnedByProxy = false` flag: every `throw` between
* `registry.create()` and `session.proxyManager = …` had to be caught by one
* catch that consulted the flag. That flag closed a real leak (#557, from the
* #552 review) — a rejected transform used to strand the slot, and ten stranded
* slots turned every later launch of that language into "Maximum adapter
* instances (10) reached", a message that says nothing about the error that
* actually caused it.
*
* The lease is *behaviour-equivalent to that flag on every path*. What it
* changes is that correctness stops being a discipline: the flag has to be
* assigned at exactly one point and consulted from exactly one catch, so the
* next `throw` site added outside that window silently reopens the leak.
* Acquire the lease, do the setup inside `try`, hand ownership to the
* ProxyManager with `transferTo`, and `release()` in `finally`. Release after a
* transfer is a no-op, so the one `finally` covers every path: a throw anywhere
* in the body disposes, a successful transfer does not, and neither depends on
* remembering to set anything. (`await using` would express this directly, but
* the project's `lib` has no `ESNext.Disposable`.)
*
* The lease covers only the setup window. After a transfer the ProxyManager is
* the owner, and disposal happens through its teardown — `ProxyManager.cleanup()`
* disposes through the same `disposeAdapterQuietly` helper, which the callers'
* catches reach by stopping `session.proxyManager` — exactly as before.
*/
import type { AdapterConfig, IAdapterRegistry, IDebugAdapter, ILogger } from '@debugmcp/shared';
import type { IProxyManagerFactory } from '../factories/proxy-manager-factory.js';
import { disposeAdapterQuietly } from './adapter-disposal.js';
import type { IProxyManager } from '../proxy/proxy-manager.js';
/**
* Where the adapter's ownership currently sits. Three states, not a boolean:
* 'transferred' and 'released' are both "not ours any more", but confusing them
* is how a caller ends up looking for a disposed adapter inside a running proxy,
* so the error a misuse raises names which one actually happened.
*/
export type AdapterLeaseState = 'held' | 'transferred' | 'released';
export class AdapterLease {
private state: AdapterLeaseState = 'held';
private constructor(
/** The leased adapter. Valid whether or not the lease is still held. */
readonly adapter: IDebugAdapter,
private readonly logger: ILogger,
private readonly sessionId: string
) {}
/**
* Create an adapter through the registry and take ownership of it.
*
* The instance limit and the 'disposed' bookkeeping stay in the registry;
* this only wraps the result in an owner that knows how to give it back.
* A rejection from `create` yields no lease — nothing was allocated.
*/
static async acquire(
registry: Pick<IAdapterRegistry, 'create'>,
language: string,
config: AdapterConfig,
logger: ILogger
): Promise<AdapterLease> {
const adapter = await registry.create(language, config);
return new AdapterLease(adapter, logger, config.sessionId);
}
/** Which of the three ownership states this lease is in. */
getState(): AdapterLeaseState {
return this.state;
}
/**
* Hand the adapter to a ProxyManager, which becomes its owner and disposer.
*
* Ownership moves only after `factory.create` returns: a throwing factory
* leaves the lease held, so the caller's `finally` still disposes.
*/
transferTo(factory: IProxyManagerFactory): IProxyManager {
if (this.state !== 'held') {
throw new Error(
`Adapter lease for session ${this.sessionId} was already ${this.state}`
);
}
const proxyManager = factory.create(this.adapter);
this.state = 'transferred';
return proxyManager;
}
/**
* Dispose the adapter if this lease still owns it. Idempotent, and a no-op
* after a transfer, so it is safe as the sole `finally` of a setup block.
*
* **Never throws, and that is load-bearing**: this is the `finally` of
* `ProxyLauncher.start`, so anything escaping here replaces the setup error
* that sent us there with a teardown error — hiding the cause the caller was
* about to report. `disposeAdapterQuietly` is what makes that true (issue
* #573); the state flips to 'released' before it runs, so a failed disposal
* still ends the lease rather than leaving it to be retried.
*/
async release(): Promise<void> {
if (this.state !== 'held') {
return;
}
this.state = 'released';
await disposeAdapterQuietly(
this.adapter,
this.logger,
`[SessionManager] Failed to dispose adapter after launch setup error for session ${this.sessionId}`
);
}
}