@celilo/console-server 0.6.1 → 0.6.2
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/package.json +1 -1
- package/src/upstream.ts +58 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@celilo/console-server",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "HTTP surface for the celilo web console. Serves the SPA and hosts the tsrpc API. Holds no database: it reaches celilo-mgr over the SSH remote API as a read-only principal.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
package/src/upstream.ts
CHANGED
|
@@ -28,7 +28,12 @@
|
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
30
|
import type { DisplayWriter } from '@celilo/cli-display';
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
type RemoteSession,
|
|
33
|
+
type RemoteTransport,
|
|
34
|
+
openRemoteSession,
|
|
35
|
+
sshTransportWith,
|
|
36
|
+
} from '@celilo/core';
|
|
32
37
|
|
|
33
38
|
/** Why a read could not be served. Every case is renderable; none is silence. */
|
|
34
39
|
export type UnavailableReason =
|
|
@@ -65,6 +70,8 @@ export interface UpstreamOptions {
|
|
|
65
70
|
/** Exit code the API uses for "this principal may not do that". */
|
|
66
71
|
const EXIT_DENIED = 126;
|
|
67
72
|
|
|
73
|
+
type RemoteOutcomeOf = Awaited<ReturnType<RemoteSession['run']>>;
|
|
74
|
+
|
|
68
75
|
interface CacheEntry {
|
|
69
76
|
/** When the value SETTLED. Meaningless while `pending`. */
|
|
70
77
|
at: number;
|
|
@@ -82,6 +89,22 @@ export class Upstream {
|
|
|
82
89
|
private readonly ttl: number;
|
|
83
90
|
private readonly now: () => number;
|
|
84
91
|
|
|
92
|
+
/**
|
|
93
|
+
* One connection, reused across reads.
|
|
94
|
+
*
|
|
95
|
+
* `api-serve` is a persistent server and its start-up dominates a read: on
|
|
96
|
+
* celilo-mgr it greets at ~1.8s and then answers commands in 1.25s and 0.06s
|
|
97
|
+
* on the SAME connection. Opening a fresh one per read re-paid that every
|
|
98
|
+
* time, which is most of why a console panel cost 5-11s against a 15s client
|
|
99
|
+
* timeout.
|
|
100
|
+
*
|
|
101
|
+
* ONE session, not a pool: the server runs a single command at a time
|
|
102
|
+
* (`serve.ts` holds one `commandRunning` slot), so a pool would not raise
|
|
103
|
+
* throughput, only connection count — and connection count is exactly what
|
|
104
|
+
* celilo#921 was about.
|
|
105
|
+
*/
|
|
106
|
+
private session: RemoteSession | null = null;
|
|
107
|
+
|
|
85
108
|
constructor(private readonly options: UpstreamOptions) {
|
|
86
109
|
this.ttl = options.cacheTtlMs ?? 2_000;
|
|
87
110
|
this.now = options.now ?? Date.now;
|
|
@@ -154,6 +177,34 @@ export class Upstream {
|
|
|
154
177
|
this.cache.clear();
|
|
155
178
|
}
|
|
156
179
|
|
|
180
|
+
private ensureSession(): RemoteSession {
|
|
181
|
+
const live = this.session;
|
|
182
|
+
if (live !== null && !live.closed) return live;
|
|
183
|
+
const session = openRemoteSession(this.options.dest, {
|
|
184
|
+
openTransport: this.options.openTransport ?? sshTransportWith(this.options.identityFile),
|
|
185
|
+
// Reached only if celilo asks the console a question. Nothing here can
|
|
186
|
+
// answer one, so say so loudly rather than parking a request forever.
|
|
187
|
+
// Session-scoped now rather than per-command, so it cannot name the argv;
|
|
188
|
+
// the reason is the same for every command the console issues.
|
|
189
|
+
renderInterview: () => {
|
|
190
|
+
throw new UpstreamUnavailable(
|
|
191
|
+
'interview',
|
|
192
|
+
[],
|
|
193
|
+
'A command raised an interview question. The console has no responder and never invokes operations that can ask one.',
|
|
194
|
+
);
|
|
195
|
+
},
|
|
196
|
+
onBlocked: 'return',
|
|
197
|
+
});
|
|
198
|
+
this.session = session;
|
|
199
|
+
return session;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Drop the connection. The next read opens a fresh one. */
|
|
203
|
+
closeSession(): void {
|
|
204
|
+
this.session?.close();
|
|
205
|
+
this.session = null;
|
|
206
|
+
}
|
|
207
|
+
|
|
157
208
|
private async runJson<T>(argv: readonly string[]): Promise<T> {
|
|
158
209
|
const output = await this.runText(argv);
|
|
159
210
|
try {
|
|
@@ -177,23 +228,14 @@ export class Upstream {
|
|
|
177
228
|
isTTY: false,
|
|
178
229
|
};
|
|
179
230
|
|
|
180
|
-
let outcome:
|
|
231
|
+
let outcome: RemoteOutcomeOf;
|
|
181
232
|
try {
|
|
182
|
-
outcome = await
|
|
183
|
-
openTransport: this.options.openTransport ?? sshTransportWith(this.options.identityFile),
|
|
184
|
-
out,
|
|
185
|
-
// Reached only if celilo asks the console a question. Nothing here can
|
|
186
|
-
// answer one, so say so loudly rather than parking a request forever.
|
|
187
|
-
renderInterview: () => {
|
|
188
|
-
throw new UpstreamUnavailable(
|
|
189
|
-
'interview',
|
|
190
|
-
argv,
|
|
191
|
-
`\`${argv.join(' ')}\` raised an interview question. The console has no responder and never invokes operations that can ask one.`,
|
|
192
|
-
);
|
|
193
|
-
},
|
|
194
|
-
onBlocked: 'return',
|
|
195
|
-
});
|
|
233
|
+
outcome = await this.ensureSession().run(argv, { out });
|
|
196
234
|
} catch (error) {
|
|
235
|
+
// The connection is suspect once a read has thrown through it — the
|
|
236
|
+
// server may have restarted or the transport died mid-command. Drop it so
|
|
237
|
+
// the NEXT read reconnects instead of writing into a pipe nobody reads.
|
|
238
|
+
this.closeSession();
|
|
197
239
|
if (error instanceof UpstreamUnavailable) throw error;
|
|
198
240
|
throw new UpstreamUnavailable(
|
|
199
241
|
'unreachable',
|